diff --git a/webclient/.gitignore b/webclient/.gitignore index d0eec90c8..ed0268c3b 100644 --- a/webclient/.gitignore +++ b/webclient/.gitignore @@ -5,6 +5,9 @@ /.pnp .pnp.js +# AI generated docs +/plans + # generated ./src files /src/proto-files.json /src/server-props.json diff --git a/webclient/CLAUDE.md b/webclient/CLAUDE.md new file mode 100644 index 000000000..8c1b59078 --- /dev/null +++ b/webclient/CLAUDE.md @@ -0,0 +1,170 @@ +# CLAUDE.md + +Guidance for Claude Code when working inside `webclient/` — the React/TypeScript SPA (Webatrice) that connects to a Servatrice server over a WebSocket. It is a self-contained application; the only thing it shares with the rest of the repo (C++ desktop/server stack) is the protobuf protocol in `../libcockatrice_protocol/`. Anything outside `webclient/` is out-of-scope unless a task explicitly touches the protocol. + +All commands below are run from this directory. + +## Commands + +```bash +npm start # Vite dev server (runs proto:generate + prebuild.js first) +npm run build # production build (same prebuild hooks) +npm test # vitest run (one-shot) +npm run test:watch # vitest watch +npm run lint # eslint src/ +npm run lint:fix +npm run golden # lint + test — the CI-equivalent gate to run before declaring work done +npm run proto:generate # `npx buf generate` — regenerates TS bindings into src/generated/proto +``` + +Single test file: `npx vitest run path/to/file.spec.ts`. Filter by name: `npx vitest run -t "partial test name"`. + +The dev server has `server.open: true`, so `npm start` pops a browser tab automatically. + +## Architecture + +The webclient is a Redux Toolkit + RxJS app. Its defining abstraction is a layered WebSocket client that speaks the Cockatrice protobuf protocol to Servatrice. Understanding the layering is essential before editing anything under `src/websocket/`, `src/api/`, or `src/store/`. + +### Protocol layer + +- **`src/generated/proto/`** — auto-generated from `../libcockatrice_protocol/**/*.proto` by `buf` (see `buf.gen.yaml`). Never edit by hand. Runtime is `@bufbuild/protobuf` (Protobuf-ES); the codebase was recently migrated off the older `protobufjs`, so if you find any stray references to the old runtime, they're bugs. +- **`src/types/` is the only public surface for generated code.** `src/types/data.ts` hand-rolls an `export *` barrel over every proto file that consumers use, and `src/types/index.ts` re-exports it as `Data`, plus `Enriched` (protocol types extended with client-only fields) and `App` (pure client types). Import as `import { Data, Enriched } from '@app/types'` and use `Data.Command_Login`, `Data.ServerInfo_User`, etc. **Never import directly from `@app/generated/proto/*` outside `src/types/`.** When a new proto file starts being consumed, add an `export *` line to `src/types/data.ts` — there is a standing TODO to replace this rollup with a protobuf-es plugin. + +### WebSocket layer (`src/websocket/`) + +A strict inbound/outbound split sits on top of a transport core: + +- **`services/`** — transport: `WebSocketService` (socket lifecycle), `ProtobufService` (encode/decode + request/response correlation), `KeepAliveService` (ping/pong), `command-options` (per-command response config). This layer has no knowledge of Redux. +- **`commands/`** — *outbound*. Organised by scope (`session/`, `room/`, `game/`, `admin/`, `moderator/`). Each command builds a protobuf request and hands it to `ProtobufService.send{Session,Room,Game,Admin,Moderator}Command` along with a `CommandOptions` describing how to handle the response. +- **`events/`** — *inbound*. Handlers for server-pushed events, same scopes. They translate protobuf events into calls on the persistence layer. +- **`persistence/`** — the **only** bridge from the websocket layer into app state. `SessionPersistence`, `RoomPersistence`, `GamePersistence`, `AdminPersistence`, `ModeratorPersistence` dispatch Redux actions and/or write to Dexie. +- **`WebClient.ts`** — singleton facade that wires the services, commands, events, and persistence together. + +**Layering invariant (enforced on this branch, not aspirational):** + +1. Containers and components call `src/api/*` services — never `src/websocket/*` directly. +2. Commands and event handlers call `*Persistence` methods — never `store.dispatch` directly. +3. Only `*.dispatch.ts` helpers inside `src/store/` and persistence code may touch the Redux store. + +If you find yourself wanting to skip a layer (dispatching from an event handler, calling a command from a container, reaching into `src/generated/proto/` from a component), stop — the refactor on `webclient-websocket-layer` exists precisely to eliminate those shortcuts. There are currently zero violations; keep it that way. + +### ProtobufService: request/response correlation + +- Every outbound `CommandContainer` gets a monotonically increasing `cmdId` (cast to `BigInt` for the proto field). A `Map` stores the response handler keyed by that ID; when `ServerMessage.RESPONSE` arrives, `processServerResponse` looks up and invokes the callback, then deletes the entry. +- There is **no timeout or retry**. `resetCommands()` (called on reconnect) zeros `cmdId` and clears the pending map, silently dropping any in-flight callbacks. Code that needs reconnection resilience has to handle it at a higher layer. +- `sendCommand` is a no-op write if the transport isn't open — it still registers the callback, so a stale pending entry can accumulate until the next reset. +- Inbound event dispatch is extension-based: `processRoomEvent` / `processSessionEvent` / `processGameEvent` iterate `RoomEvents` / `SessionEvents` / `GameEvents` (tuples of `[extension, handler]`) and invoke the first handler whose extension is set on the message. Adding a new event handler means appending to those arrays. + +### command-options contract (`src/websocket/services/command-options.ts`) + +Every `send*Command` call accepts an optional `CommandOptions`: + +- `responseExt?: GenExtension` — the response payload extension to unwrap on success. +- `onSuccess?: (response: R, raw: Response) => void` — called when `responseCode === RespOk`. If `responseExt` is absent, the overload becomes `() => void`. +- `onResponseCode?: { [code: number]: (raw: Response) => void }` — per-error-code handlers. +- `onError?: (code: number, raw: Response) => void` — fallback for codes not in `onResponseCode`. +- `onResponse?: (raw: Response) => void` — if set, it handles the raw response and bypasses every other hook. Use this when you need the full response object regardless of code. + +If none of the hooks fire for a non-OK response, `handleResponse` logs the failure via `console.error` with the command's proto type name. The practical rule: `onSuccess` funnels into persistence, `onError` funnels into persistence (usually to flip connection state or show a toast), and `onResponse` is rare. + +### Public API for UI (`src/api/`) + +Thin service wrappers (`AuthenticationService`, `SessionService`, `RoomsService`, `GameService`, `ModeratorService`, `AdminService`) that expose websocket commands to UI code. A few things to know: + +- **All command methods are `static` and return `void`.** They're fire-and-forget — the response flows back through the `command-options` callbacks plumbed inside the command itself, into persistence, into the store. Don't try to await them. +- A handful of methods return `boolean` (e.g. `AuthenticationService.isConnected`, `isModerator`) — those are pure sync predicates, not command sends. +- Files use the `.tsx` extension even though they contain no JSX. That's a leftover convention; don't "fix" it. + +### State (`src/store/`) + +Redux Toolkit store (`store.ts`, `rootReducer.ts`) split by feature. Each slice follows the same file layout: + +- `*.actions.ts` — action creators +- `*.reducer.ts` — slice reducer +- `*.selectors.ts` — selectors (mostly plain getters; `createSelector` only for derived lists) +- `*.dispatch.ts` — dispatch helpers called by the persistence layer +- `*.interfaces.ts` / `*.types.ts` — state shape and enums + +Slices: `server/`, `rooms/`, `game/`, plus shared `actions/` and `common/` helpers (`SortUtil`, `normalizers`). Consumers import through the `@app/store` barrel — `GameSelectors`, `GameDispatch`, `GameTypes`, and the same prefixed set for `Server`/`Rooms`. **Don't deep-import from `src/store/game/game.selectors.ts` etc.** — go through `@app/store`. + +Shape notes worth knowing before you touch a reducer: + +- `game/` is deeply normalized: `games[gameId].players[playerId].zones[zoneName].cards`. Selectors are plain getters so lookups stay O(1); `createSelector` is reserved for the few that build derived lists (e.g. `getActiveGameIds`). +- Selectors return module-scope `EMPTY_ARRAY` / `EMPTY_OBJECT` constants for missing data to preserve referential equality and avoid spurious re-renders. +- `rooms/` is *partially* normalized: rooms are keyed by ID, but each room also carries denormalized `gameList` / `userList` arrays. Server updates often omit those lists, so the reducer merges new metadata while preserving the existing arrays. There is a standing TODO to clean this up. +- `server/` is mostly flat maps keyed by username (`messages`, `userInfo`, buddy/ignore lists) plus connection state. + +### Local persistence (`src/services/dexie/`) + +IndexedDB storage via Dexie for cards, sets, tokens, known hosts, and settings. DTOs live in `DexieDTOs/`. This is separate from the Redux store — used for data that should survive a reload (card database, user settings, host list). Dexie is not mocked in unit tests; code that writes to Dexie is typically exercised only in integration paths. + +### UI + +- **`containers/`** — route-level, Redux-connected. Top-level routes: `App`, `Initialize`, `Login`, `Server`, `Room`, `Game`, `Player`, `Decks`, `Account`, `Logs`, `Layout`, `Unsupported`. Routing lives in `containers/App/AppShellRoutes.tsx`. +- **`components/`** — presentational, mostly unconnected. +- **`forms/`** — `react-final-form` forms (e.g. `LoginForm`). +- **`dialogs/`** — MUI dialogs. +- **`hooks/`** — shared hooks (e.g. `useAutoConnect`). +- **`i18n.ts` / `i18n-backend.ts`** — `react-i18next` + ICU; translations managed via Transifex. +- UI kit: MUI v7 (`@mui/material`, `@emotion`). + +### Path aliases + +`tsconfig.json` defines the following (resolved at build time by `vite-tsconfig-paths`): + +``` +@app/api @app/components @app/containers @app/dialogs +@app/forms @app/hooks @app/images @app/services +@app/store @app/types @app/websocket @app/generated/* +``` + +Prefer these in new code over relative imports when crossing top-level directory boundaries. Deep paths into a barrel target (e.g. `@app/store/game/...`) are a smell — add the symbol to the relevant `index.ts` barrel instead. + +### End-to-end data flow + +User action in a container → `src/api/*Service` → `src/websocket/commands/*` → `ProtobufService.send*Command` → socket. +Server reply/event → `src/websocket/events/*` (or the `command-options` callback on the original command) → `src/websocket/persistence/*` → `*.dispatch.ts` helpers → Redux / Dexie → selectors → container re-render. + +## Build pipeline and generated files + +`npm start` and `npm run build` both run `prestart`/`prebuild` hooks that invoke `proto:generate` and then `node prebuild.js`. `prebuild.js` does three things: + +1. Copies shared country flag assets from `../cockatrice/resources/countries` into `src/images/countries`. +2. Writes `src/server-props.json` containing `REACT_APP_VERSION` = current `git rev-parse HEAD`. +3. Walks `src/**/*.i18n.json`, merges them into `src/i18n-default.json`, and **throws on duplicate keys** (`i18n key collision: ${key}`). Namespace your i18n keys — collisions fail the build. + +Files you should never edit by hand (all auto-generated, all committed): + +- `src/generated/proto/**` +- `src/i18n-default.json` +- `src/server-props.json` + +If `npm start` seems to be ignoring a new `.i18n.json` file or a fresh proto, run `npm run proto:generate && node prebuild.js` directly — the hooks only fire on `start`/`build`, not on `test` or `lint`. + +`.env.development`, `.env.production`, and `.env.test` exist but are empty. There is currently no env-var configuration surface; server URLs and the like are resolved through the login UI / `server-props.json`, not `import.meta.env`. + +## Testing + +Vitest + Testing Library + jsdom; `setupTests.ts` registers jest-dom matchers. + +**Vitest runs with `test.isolate: false`.** Every spec file in a worker shares the same module graph, so `vi.mock(...)` factories and any mocks they create persist across tests. Consequences: + +- The global `afterEach` in `setupTests.ts` calls `vi.clearAllMocks()` + `vi.restoreAllMocks()` + `vi.useRealTimers()`. It deliberately does **not** call `vi.resetAllMocks()`, because that would reset the implementations of `vi.fn()` instances created inside `vi.mock(...)` factories and break every spec that mocks `store.dispatch` once at file load. +- A test that installs a custom `mockReturnValue` / `mockImplementation` should not assume the next test resets it — either overwrite it or rely on `clearAllMocks` wiping only call histories. +- Always use real timers at the end of a test that switched to fake ones; the global teardown will catch leaks, but relying on it is fragile across files. + +Other conventions: + +- **Fixtures.** Store slices have co-located `__mocks__/fixtures.ts` files (notably `src/store/game/__mocks__/fixtures.ts`) exposing factories like `makeCard`, `makeGameEntry`, `makePlayerProperties`, `makeState`. They build protobuf messages via `create(Schema, overrides)`. Reuse them in new tests instead of hand-rolling proto objects. +- **Websocket mocks.** `src/websocket/__mocks__/` holds shared mock builders (e.g. `makeMockWebSocket`, `makeWebClientMock`, `makeSessionPersistenceMock`). Command and event specs install these with `vi.mock(...)` at the top of the file. +- **Slice tests are per-concern.** Each slice ships parallel `*.actions.spec.ts`, `*.reducer.spec.ts`, `*.selectors.spec.ts`, and `*.dispatch.spec.ts` files; tests don't cross concerns. + +`npm run golden` (lint + test) is the CI gate — run it before declaring work done. + +## Protocol changes + +When a task requires editing `.proto` files in `../libcockatrice_protocol/`, run `npm run proto:generate` afterwards, and: + +1. If the change introduces a new proto *file* that code outside `src/types/` needs to consume, add an `export *` line for it in `src/types/data.ts`. +2. Update any command/event/persistence code that consumes the changed messages. +3. Commit the regenerated files under `src/generated/proto/`. diff --git a/webclient/buf.gen.plugin.mjs b/webclient/buf.gen.plugin.mjs new file mode 100644 index 000000000..b618761db --- /dev/null +++ b/webclient/buf.gen.plugin.mjs @@ -0,0 +1,99 @@ +// @ts-check +/** + * Custom protoc-gen-es sibling plugin. Emits `src/generated/index.ts`, a + * single rollup that re-exports every generated `_pb` module and adds + * `MessageInitShape` param aliases for every `Command_*` message. + * + * Wired into `buf.gen.yaml` as a second local plugin. Runs with the same + * descriptor set protoc-gen-es consumes, so output always tracks the protos. + */ +import { createEcmaScriptPlugin, runNodeJs } from '@bufbuild/protoplugin'; + +const HEADER = [ + '// @generated by protoc-gen-data. DO NOT EDIT.', + '// Rollup of all proto modules + MessageInitShape param aliases for every Command_*.', + '/* eslint-disable */', + '', + '', +].join('\n'); + +const inner = createEcmaScriptPlugin({ + name: 'protoc-gen-data', + version: 'v0.1.0', + generateTs(schema) { + const f = schema.generateFile('index.ts'); + + const MessageInitShape = f.import('MessageInitShape', '@bufbuild/protobuf', true); + const MessageType = f.import('Message', '@bufbuild/protobuf', true); + const GenExtensionType = f.import('GenExtension', '@bufbuild/protobuf/codegenv2', true); + + const sortedFiles = [...schema.files].sort((a, b) => a.name.localeCompare(b.name)); + + for (const file of sortedFiles) { + f.print('export * from ', f.string(`./proto/${file.name}_pb`), ';'); + } + f.print(); + + const commandMessages = []; + for (const file of sortedFiles) { + for (const msg of file.messages) { + if (msg.name.startsWith('Command_')) { + commandMessages.push(msg); + } + } + } + commandMessages.sort((a, b) => a.name.localeCompare(b.name)); + + // importSchema() resolves paths relative to this plugin's `out` dir, which + // yields `./_pb` — but the _pb files live under ./proto/ (protoc-gen-es's + // out). Build the import path explicitly so it points inside the proto subdir. + for (const msg of commandMessages) { + const alias = msg.name.slice('Command_'.length) + 'Params'; + const schemaName = `${msg.name}Schema`; + const schemaSym = f.import(schemaName, `./proto/${msg.file.name}_pb`, true); + f.print('export type ', alias, ' = ', MessageInitShape, ';'); + } + f.print(); + + // Generic extension registry infrastructure. Consolidates the three + // near-duplicate registry types and helpers that used to live in + // src/websocket/services/protobuf-types.ts into one generic pair. + // Specialised aliases (Session/Room/Game) still live in protobuf-types.ts + // because GameExtensionRegistry needs GameEventMeta — a hand-written + // domain type whose import would create a generated/ ↔ types/ cycle. + f.print('export type RegistryEntry = ['); + f.print(' ', GenExtensionType, ','); + f.print(' (value: V, meta: M) => void,'); + f.print('];'); + f.print(); + // Return type widens V to `unknown` so the heterogeneous entries that + // callers build can be stored in a homogeneous `RegistryEntry[]` + // array. This is the actual value-add over a bare tuple literal. + f.print('export function makeEntry('); + f.print(' ext: ', GenExtensionType, ','); + f.print(' handler: (value: V, meta: M) => void,'); + f.print('): RegistryEntry {'); + f.print(' return [ext, handler] as unknown as RegistryEntry;'); + f.print('}'); + }, +}); + +// Skip f.preamble() above and inject a custom rollup-aware header here instead — +// preamble() would write "@generated from file X.proto" which is misleading for +// a rollup file built from every input proto. +/** @type {import('@bufbuild/protoplugin').Plugin} */ +const plugin = { + name: inner.name, + version: inner.version, + run(request) { + const response = inner.run(request); + for (const file of response.file) { + if (file.name === 'index.ts' && typeof file.content === 'string') { + file.content = HEADER + file.content; + } + } + return response; + }, +}; + +runNodeJs(plugin); diff --git a/webclient/buf.gen.yaml b/webclient/buf.gen.yaml index eba5ea4ad..641ffe58a 100644 --- a/webclient/buf.gen.yaml +++ b/webclient/buf.gen.yaml @@ -6,3 +6,7 @@ plugins: out: src/generated/proto opt: - target=ts + - local: [node, buf.gen.plugin.mjs] + out: src/generated + opt: + - target=ts diff --git a/webclient/src/api/AdminService.spec.ts b/webclient/src/api/AdminService.spec.ts index d14207629..1964a8368 100644 --- a/webclient/src/api/AdminService.spec.ts +++ b/webclient/src/api/AdminService.spec.ts @@ -1,4 +1,4 @@ -vi.mock('websocket', () => ({ +vi.mock('@app/websocket', () => ({ AdminCommands: { adjustMod: vi.fn(), reloadConfig: vi.fn(), @@ -8,9 +8,7 @@ vi.mock('websocket', () => ({ })); import { AdminService } from './AdminService'; -import { AdminCommands } from 'websocket'; - -beforeEach(() => vi.clearAllMocks()); +import { AdminCommands } from '@app/websocket'; describe('AdminService', () => { describe('adjustMod', () => { diff --git a/webclient/src/api/AdminService.tsx b/webclient/src/api/AdminService.tsx index 623ad546a..c280fca7b 100644 --- a/webclient/src/api/AdminService.tsx +++ b/webclient/src/api/AdminService.tsx @@ -1,4 +1,4 @@ -import { AdminCommands } from 'websocket'; +import { AdminCommands } from '@app/websocket'; export class AdminService { static adjustMod(userName: string, shouldBeMod?: boolean, shouldBeJudge?: boolean): void { diff --git a/webclient/src/api/AuthenticationService.spec.ts b/webclient/src/api/AuthenticationService.spec.ts index 9d9dfcd79..0b2dfbc1c 100644 --- a/webclient/src/api/AuthenticationService.spec.ts +++ b/webclient/src/api/AuthenticationService.spec.ts @@ -1,71 +1,113 @@ -vi.mock('websocket', () => ({ +vi.mock('@app/websocket', () => ({ SessionCommands: { connect: vi.fn(), disconnect: vi.fn(), }, })); -vi.mock('generated/proto/serverinfo_user_pb', () => ({ - ServerInfo_User_UserLevelFlag: { - IsModerator: 4, - }, -})); +vi.mock('../generated/proto/serverinfo_user_pb', async (importOriginal) => { + const actual = await importOriginal(); + return { + ...actual, + ServerInfo_User_UserLevelFlag: { + IsModerator: 4, + }, + }; +}); import { AuthenticationService } from './AuthenticationService'; -import { SessionCommands } from 'websocket'; -import { StatusEnum, WebSocketConnectOptions, WebSocketConnectReason } from 'types'; +import { SessionCommands } from '@app/websocket'; +import { App, Data } from '@app/types'; +import { create } from '@bufbuild/protobuf'; -const testOptions: WebSocketConnectOptions = { host: 'localhost', port: '4748', userName: 'user', password: 'pw' }; - -beforeEach(() => vi.clearAllMocks()); +const baseTransport = { host: 'localhost', port: '4748' }; describe('AuthenticationService', () => { describe('login', () => { it('calls SessionCommands.connect with LOGIN reason', () => { - AuthenticationService.login(testOptions); - expect(SessionCommands.connect).toHaveBeenCalledWith(testOptions, WebSocketConnectReason.LOGIN); + AuthenticationService.login({ ...baseTransport, userName: 'user', password: 'pw' }); + expect(SessionCommands.connect).toHaveBeenCalledWith( + expect.objectContaining({ + ...baseTransport, + userName: 'user', + password: 'pw', + reason: App.WebSocketConnectReason.LOGIN, + }) + ); }); }); describe('testConnection', () => { it('calls SessionCommands.connect with TEST_CONNECTION reason', () => { - AuthenticationService.testConnection(testOptions); - expect(SessionCommands.connect).toHaveBeenCalledWith(testOptions, WebSocketConnectReason.TEST_CONNECTION); + AuthenticationService.testConnection(baseTransport); + expect(SessionCommands.connect).toHaveBeenCalledWith( + expect.objectContaining({ ...baseTransport, reason: App.WebSocketConnectReason.TEST_CONNECTION }) + ); }); }); describe('register', () => { it('calls SessionCommands.connect with REGISTER reason', () => { - AuthenticationService.register(testOptions); - expect(SessionCommands.connect).toHaveBeenCalledWith(testOptions, WebSocketConnectReason.REGISTER); + AuthenticationService.register({ + ...baseTransport, + userName: 'user', + password: 'pw', + email: 'a@b.com', + country: 'US', + realName: 'User', + }); + expect(SessionCommands.connect).toHaveBeenCalledWith( + expect.objectContaining({ userName: 'user', reason: App.WebSocketConnectReason.REGISTER }) + ); }); }); describe('activateAccount', () => { it('calls SessionCommands.connect with ACTIVATE_ACCOUNT reason', () => { - AuthenticationService.activateAccount(testOptions); - expect(SessionCommands.connect).toHaveBeenCalledWith(testOptions, WebSocketConnectReason.ACTIVATE_ACCOUNT); + AuthenticationService.activateAccount({ + ...baseTransport, + userName: 'user', + token: 'tok', + }); + expect(SessionCommands.connect).toHaveBeenCalledWith( + expect.objectContaining({ token: 'tok', reason: App.WebSocketConnectReason.ACTIVATE_ACCOUNT }) + ); }); }); describe('resetPasswordRequest', () => { it('calls SessionCommands.connect with PASSWORD_RESET_REQUEST reason', () => { - AuthenticationService.resetPasswordRequest(testOptions); - expect(SessionCommands.connect).toHaveBeenCalledWith(testOptions, WebSocketConnectReason.PASSWORD_RESET_REQUEST); + AuthenticationService.resetPasswordRequest({ ...baseTransport, userName: 'user' }); + expect(SessionCommands.connect).toHaveBeenCalledWith( + expect.objectContaining({ userName: 'user', reason: App.WebSocketConnectReason.PASSWORD_RESET_REQUEST }) + ); }); }); describe('resetPasswordChallenge', () => { it('calls SessionCommands.connect with PASSWORD_RESET_CHALLENGE reason', () => { - AuthenticationService.resetPasswordChallenge(testOptions); - expect(SessionCommands.connect).toHaveBeenCalledWith(testOptions, WebSocketConnectReason.PASSWORD_RESET_CHALLENGE); + AuthenticationService.resetPasswordChallenge({ + ...baseTransport, + userName: 'user', + email: 'a@b.com', + }); + expect(SessionCommands.connect).toHaveBeenCalledWith( + expect.objectContaining({ email: 'a@b.com', reason: App.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE }) + ); }); }); describe('resetPassword', () => { it('calls SessionCommands.connect with PASSWORD_RESET reason', () => { - AuthenticationService.resetPassword(testOptions); - expect(SessionCommands.connect).toHaveBeenCalledWith(testOptions, WebSocketConnectReason.PASSWORD_RESET); + AuthenticationService.resetPassword({ + ...baseTransport, + userName: 'user', + token: 'tok', + newPassword: 'newpw', + }); + expect(SessionCommands.connect).toHaveBeenCalledWith( + expect.objectContaining({ newPassword: 'newpw', reason: App.WebSocketConnectReason.PASSWORD_RESET }) + ); }); }); @@ -78,41 +120,41 @@ describe('AuthenticationService', () => { describe('isConnected', () => { it('returns true when state is LOGGED_IN', () => { - expect(AuthenticationService.isConnected(StatusEnum.LOGGED_IN)).toBe(true); + expect(AuthenticationService.isConnected(App.StatusEnum.LOGGED_IN)).toBe(true); }); it('returns false when state is DISCONNECTED', () => { - expect(AuthenticationService.isConnected(StatusEnum.DISCONNECTED)).toBe(false); + expect(AuthenticationService.isConnected(App.StatusEnum.DISCONNECTED)).toBe(false); }); it('returns false when state is CONNECTING', () => { - expect(AuthenticationService.isConnected(StatusEnum.CONNECTING)).toBe(false); + expect(AuthenticationService.isConnected(App.StatusEnum.CONNECTING)).toBe(false); }); it('returns false when state is CONNECTED', () => { - expect(AuthenticationService.isConnected(StatusEnum.CONNECTED)).toBe(false); + expect(AuthenticationService.isConnected(App.StatusEnum.CONNECTED)).toBe(false); }); it('returns false when state is LOGGING_IN', () => { - expect(AuthenticationService.isConnected(StatusEnum.LOGGING_IN)).toBe(false); + expect(AuthenticationService.isConnected(App.StatusEnum.LOGGING_IN)).toBe(false); }); }); describe('isModerator', () => { it('returns true when userLevel has the IsModerator bit set', () => { - expect(AuthenticationService.isModerator({ userLevel: 4 } as any)).toBe(true); + expect(AuthenticationService.isModerator(create(Data.ServerInfo_UserSchema, { userLevel: 4 }))).toBe(true); }); it('returns true when userLevel has IsModerator and other bits set', () => { - expect(AuthenticationService.isModerator({ userLevel: 7 } as any)).toBe(true); + expect(AuthenticationService.isModerator(create(Data.ServerInfo_UserSchema, { userLevel: 7 }))).toBe(true); }); it('returns false when userLevel does not have the IsModerator bit', () => { - expect(AuthenticationService.isModerator({ userLevel: 1 } as any)).toBe(false); + expect(AuthenticationService.isModerator(create(Data.ServerInfo_UserSchema, { userLevel: 1 }))).toBe(false); }); it('returns false for admin-only userLevel without moderator bit', () => { - expect(AuthenticationService.isModerator({ userLevel: 8 } as any)).toBe(false); + expect(AuthenticationService.isModerator(create(Data.ServerInfo_UserSchema, { userLevel: 8 }))).toBe(false); }); }); diff --git a/webclient/src/api/AuthenticationService.tsx b/webclient/src/api/AuthenticationService.tsx index e88226dca..bacc8e350 100644 --- a/webclient/src/api/AuthenticationService.tsx +++ b/webclient/src/api/AuthenticationService.tsx @@ -1,34 +1,33 @@ -import { StatusEnum, WebSocketConnectReason, WebSocketConnectOptions } from 'types'; -import { SessionCommands } from 'websocket'; -import { ServerInfo_User, ServerInfo_User_UserLevelFlag } from 'generated/proto/serverinfo_user_pb'; +import { App, Data, Enriched } from '@app/types'; +import { SessionCommands } from '@app/websocket'; export class AuthenticationService { - static login(options: WebSocketConnectOptions): void { - SessionCommands.connect(options, WebSocketConnectReason.LOGIN); + static login(options: Omit): void { + SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.LOGIN }); } - static testConnection(options: WebSocketConnectOptions): void { - SessionCommands.connect(options, WebSocketConnectReason.TEST_CONNECTION); + static testConnection(options: Omit): void { + SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.TEST_CONNECTION }); } - static register(options: WebSocketConnectOptions): void { - SessionCommands.connect(options, WebSocketConnectReason.REGISTER); + static register(options: Omit): void { + SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.REGISTER }); } - static activateAccount(options: WebSocketConnectOptions): void { - SessionCommands.connect(options, WebSocketConnectReason.ACTIVATE_ACCOUNT); + static activateAccount(options: Omit): void { + SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.ACTIVATE_ACCOUNT }); } - static resetPasswordRequest(options: WebSocketConnectOptions): void { - SessionCommands.connect(options, WebSocketConnectReason.PASSWORD_RESET_REQUEST); + static resetPasswordRequest(options: Omit): void { + SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.PASSWORD_RESET_REQUEST }); } - static resetPasswordChallenge(options: WebSocketConnectOptions): void { - SessionCommands.connect(options, WebSocketConnectReason.PASSWORD_RESET_CHALLENGE); + static resetPasswordChallenge(options: Omit): void { + SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE }); } - static resetPassword(options: WebSocketConnectOptions): void { - SessionCommands.connect(options, WebSocketConnectReason.PASSWORD_RESET); + static resetPassword(options: Omit): void { + SessionCommands.connect({ ...options, reason: App.WebSocketConnectReason.PASSWORD_RESET }); } static disconnect(): void { @@ -36,11 +35,11 @@ export class AuthenticationService { } static isConnected(state: number): boolean { - return state === StatusEnum.LOGGED_IN; + return state === App.StatusEnum.LOGGED_IN; } - static isModerator(user: ServerInfo_User): boolean { - const moderatorLevel = ServerInfo_User_UserLevelFlag.IsModerator; + static isModerator(user: Data.ServerInfo_User): boolean { + const moderatorLevel = Data.ServerInfo_User_UserLevelFlag.IsModerator; // @TODO tell cockatrice not to do this so shittily return (user.userLevel & moderatorLevel) === moderatorLevel; } diff --git a/webclient/src/api/ModeratorService.spec.ts b/webclient/src/api/ModeratorService.spec.ts index 00c92d447..f32a58d09 100644 --- a/webclient/src/api/ModeratorService.spec.ts +++ b/webclient/src/api/ModeratorService.spec.ts @@ -1,4 +1,4 @@ -vi.mock('websocket', () => ({ +vi.mock('@app/websocket', () => ({ ModeratorCommands: { banFromServer: vi.fn(), getBanHistory: vi.fn(), @@ -10,10 +10,8 @@ vi.mock('websocket', () => ({ })); import { ModeratorService } from './ModeratorService'; -import { ModeratorCommands } from 'websocket'; -import { LogFilters } from 'types'; - -beforeEach(() => vi.clearAllMocks()); +import { ModeratorCommands } from '@app/websocket'; +import { Data } from '@app/types'; describe('ModeratorService', () => { describe('banFromServer', () => { @@ -55,7 +53,7 @@ describe('ModeratorService', () => { describe('viewLogHistory', () => { it('delegates to ModeratorCommands.viewLogHistory', () => { - const filters: LogFilters = { dateRange: 7, userName: 'alice' }; + const filters: Data.ViewLogHistoryParams = { dateRange: 7, userName: 'alice' }; ModeratorService.viewLogHistory(filters); expect(ModeratorCommands.viewLogHistory).toHaveBeenCalledWith(filters); }); diff --git a/webclient/src/api/ModeratorService.tsx b/webclient/src/api/ModeratorService.tsx index 6c22ee55e..2fa9e9019 100644 --- a/webclient/src/api/ModeratorService.tsx +++ b/webclient/src/api/ModeratorService.tsx @@ -1,5 +1,5 @@ -import { ModeratorCommands } from 'websocket'; -import { LogFilters } from 'types'; +import { ModeratorCommands } from '@app/websocket'; +import { Data } from '@app/types'; export class ModeratorService { static banFromServer(minutes: number, userName?: string, address?: string, reason?: string, @@ -19,7 +19,7 @@ export class ModeratorService { ModeratorCommands.getWarnList(modName, userName, userClientid); } - static viewLogHistory(filters: LogFilters): void { + static viewLogHistory(filters: Data.ViewLogHistoryParams): void { ModeratorCommands.viewLogHistory(filters); } diff --git a/webclient/src/api/RoomsService.spec.ts b/webclient/src/api/RoomsService.spec.ts index 541efb904..80ff8d4cd 100644 --- a/webclient/src/api/RoomsService.spec.ts +++ b/webclient/src/api/RoomsService.spec.ts @@ -1,4 +1,4 @@ -vi.mock('websocket', () => ({ +vi.mock('@app/websocket', () => ({ SessionCommands: { joinRoom: vi.fn(), }, @@ -9,9 +9,7 @@ vi.mock('websocket', () => ({ })); import { RoomsService } from './RoomsService'; -import { RoomCommands, SessionCommands } from 'websocket'; - -beforeEach(() => vi.clearAllMocks()); +import { RoomCommands, SessionCommands } from '@app/websocket'; describe('RoomsService', () => { describe('joinRoom', () => { diff --git a/webclient/src/api/RoomsService.tsx b/webclient/src/api/RoomsService.tsx index bfb63d92a..b2f9ddc4e 100644 --- a/webclient/src/api/RoomsService.tsx +++ b/webclient/src/api/RoomsService.tsx @@ -1,4 +1,4 @@ -import { RoomCommands, SessionCommands } from 'websocket'; +import { RoomCommands, SessionCommands } from '@app/websocket'; export class RoomsService { static joinRoom(roomId: number): void { diff --git a/webclient/src/api/SessionService.spec.ts b/webclient/src/api/SessionService.spec.ts index 8e67219bc..879d4c3ef 100644 --- a/webclient/src/api/SessionService.spec.ts +++ b/webclient/src/api/SessionService.spec.ts @@ -1,4 +1,4 @@ -vi.mock('websocket', () => ({ +vi.mock('@app/websocket', () => ({ SessionCommands: { addToBuddyList: vi.fn(), removeFromBuddyList: vi.fn(), @@ -14,9 +14,7 @@ vi.mock('websocket', () => ({ })); import { SessionService } from './SessionService'; -import { SessionCommands } from 'websocket'; - -beforeEach(() => vi.clearAllMocks()); +import { SessionCommands } from '@app/websocket'; describe('SessionService', () => { describe('addToBuddyList', () => { diff --git a/webclient/src/api/SessionService.tsx b/webclient/src/api/SessionService.tsx index 2787f098d..129cdfc58 100644 --- a/webclient/src/api/SessionService.tsx +++ b/webclient/src/api/SessionService.tsx @@ -1,4 +1,4 @@ -import { SessionCommands } from 'websocket'; +import { SessionCommands } from '@app/websocket'; export class SessionService { static addToBuddyList(userName: string) { diff --git a/webclient/src/components/Card/Card.tsx b/webclient/src/components/Card/Card.tsx index f89622c3b..83af90e1f 100644 --- a/webclient/src/components/Card/Card.tsx +++ b/webclient/src/components/Card/Card.tsx @@ -1,7 +1,7 @@ // eslint-disable-next-line import React, { useMemo, useState } from 'react'; -import { CardDTO } from 'services'; +import { CardDTO } from '@app/services'; import './Card.css'; diff --git a/webclient/src/components/CardDetails/CardDetails.tsx b/webclient/src/components/CardDetails/CardDetails.tsx index 84196fda6..81a5c7fa1 100644 --- a/webclient/src/components/CardDetails/CardDetails.tsx +++ b/webclient/src/components/CardDetails/CardDetails.tsx @@ -1,7 +1,7 @@ // eslint-disable-next-line import React, { useMemo, useState } from 'react'; -import { CardDTO } from 'services'; +import { CardDTO } from '@app/services'; import Card from '../Card/Card'; diff --git a/webclient/src/components/CountryDropdown/CountryDropdown.tsx b/webclient/src/components/CountryDropdown/CountryDropdown.tsx index 09b1cf71e..5a7d519bf 100644 --- a/webclient/src/components/CountryDropdown/CountryDropdown.tsx +++ b/webclient/src/components/CountryDropdown/CountryDropdown.tsx @@ -4,9 +4,9 @@ import FormControl from '@mui/material/FormControl'; import InputLabel from '@mui/material/InputLabel'; import { useTranslation } from 'react-i18next'; -import { useLocaleSort } from 'hooks'; -import { Images } from 'images/Images'; -import { countryCodes } from 'types'; +import { useLocaleSort } from '@app/hooks'; +import { Images } from '@app/images'; +import { App } from '@app/types'; import './CountryDropdown.css'; @@ -18,7 +18,7 @@ const CountryDropdown = ({ input: { onChange } }) => { useEffect(() => onChange(value), [value]); const translateCountry = country => t(`Common.countries.${country}`); - const sortedCountries = useLocaleSort(countryCodes, translateCountry); + const sortedCountries = useLocaleSort(App.countryCodes, translateCountry); return ( diff --git a/webclient/src/components/Guard/AuthGuard.tsx b/webclient/src/components/Guard/AuthGuard.tsx index 3b0790a62..bf1ff1876 100644 --- a/webclient/src/components/Guard/AuthGuard.tsx +++ b/webclient/src/components/Guard/AuthGuard.tsx @@ -1,15 +1,15 @@ import React from 'react'; import { Navigate } from 'react-router-dom'; -import { ServerSelectors } from 'store'; -import { RouteEnum } from 'types'; -import { useAppSelector } from 'store/store'; -import { AuthenticationService } from 'api'; +import { ServerSelectors } from '@app/store'; +import { App } from '@app/types'; +import { useAppSelector } from '@app/store'; +import { AuthenticationService } from '@app/api'; const AuthGuard = () => { const state = useAppSelector(s => ServerSelectors.getState(s)); return !AuthenticationService.isConnected(state) - ? + ? :
; }; diff --git a/webclient/src/components/Guard/ModGuard.tsx b/webclient/src/components/Guard/ModGuard.tsx index f8f629898..18b68adf1 100644 --- a/webclient/src/components/Guard/ModGuard.tsx +++ b/webclient/src/components/Guard/ModGuard.tsx @@ -1,15 +1,15 @@ import React from 'react'; import { Navigate } from 'react-router-dom'; -import { ServerSelectors } from 'store'; -import { AuthenticationService } from 'api'; -import { RouteEnum } from 'types'; -import { useAppSelector } from 'store/store'; +import { ServerSelectors } from '@app/store'; +import { AuthenticationService } from '@app/api'; +import { App } from '@app/types'; +import { useAppSelector } from '@app/store'; const ModGuard = () => { const user = useAppSelector(state => ServerSelectors.getUser(state)); return !AuthenticationService.isModerator(user) - ? + ? : <>; }; diff --git a/webclient/src/components/InputAction/InputAction.tsx b/webclient/src/components/InputAction/InputAction.tsx index 6c5e61474..7326c0a35 100644 --- a/webclient/src/components/InputAction/InputAction.tsx +++ b/webclient/src/components/InputAction/InputAction.tsx @@ -2,7 +2,7 @@ import React from 'react'; import { Field } from 'react-final-form' import Button from '@mui/material/Button'; -import { InputField } from 'components'; +import { InputField } from '..'; import './InputAction.css'; diff --git a/webclient/src/components/KnownHosts/KnownHosts.tsx b/webclient/src/components/KnownHosts/KnownHosts.tsx index cb19d9f56..096a8bece 100644 --- a/webclient/src/components/KnownHosts/KnownHosts.tsx +++ b/webclient/src/components/KnownHosts/KnownHosts.tsx @@ -13,13 +13,13 @@ import AddIcon from '@mui/icons-material/Add'; import EditRoundedIcon from '@mui/icons-material/Edit'; import ErrorOutlinedIcon from '@mui/icons-material/ErrorOutlined'; -import { AuthenticationService } from 'api'; -import { KnownHostDialog } from 'dialogs'; -import { useReduxEffect } from 'hooks'; -import { HostDTO } from 'services'; -import { ServerTypes } from 'store'; -import { DefaultHosts, Host, getHostPort } from 'types'; -import Toast from 'components/Toast/Toast'; +import { AuthenticationService } from '@app/api'; +import { KnownHostDialog } from '@app/dialogs'; +import { useReduxEffect } from '@app/hooks'; +import { HostDTO } from '@app/services'; +import { ServerTypes } from '@app/store'; +import { App } from '@app/types'; +import Toast from '../Toast/Toast'; import './KnownHosts.css'; @@ -86,7 +86,7 @@ const KnownHosts = (props) => { if (!hosts?.length) { // @TODO: find a better pattern to seeding default data in indexedDB - await HostDTO.bulkAdd(DefaultHosts); + await HostDTO.bulkAdd(App.DefaultHosts); loadKnownHosts(); } else { const selectedHost = hosts.find(({ lastSelected }) => lastSelected) || hosts[0]; @@ -159,7 +159,7 @@ const KnownHosts = (props) => { })); setShowEditToast(true) } else { - const newHost: Host = { name, host, port, editable: true }; + const newHost: App.Host = { name, host, port, editable: true }; newHost.id = await HostDTO.add(newHost) as number; setHostsState(s => ({ @@ -196,7 +196,7 @@ const KnownHosts = (props) => { const testConnection = () => { setTestingConnection(TestConnection.TESTING); - const options = { ...getHostPort(hostsState.selectedHost) }; + const options = { ...App.getHostPort(hostsState.selectedHost) }; AuthenticationService.testConnection(options); } @@ -236,34 +236,38 @@ const KnownHosts = (props) => { { - hostsState.hosts.map((host, index) => ( - -
-
-
- { - testingConnection === TestConnection.FAILED - ? - : - } + hostsState.hosts.map((host, index) => { + const hostPort = App.getHostPort(hostsState.hosts[index]); + + return ( + +
+
+
+ { + testingConnection === TestConnection.FAILED + ? + : + } +
+ +
+ + {host.name} ({ hostPort.host }:{hostPort.port}) +
-
- - {host.name} ({ getHostPort(hostsState.hosts[index]).host }:{getHostPort(hostsState.hosts[index]).port}) -
+ { host.editable && ( + { + openEditKnownHostDialog(hostsState.hosts[index]); + }}> + + + ) }
- - { host.editable && ( - { - openEditKnownHostDialog(hostsState.hosts[index]); - }}> - - - ) } -
- - )) + + ); + }) } diff --git a/webclient/src/components/LanguageDropdown/LanguageDropdown.tsx b/webclient/src/components/LanguageDropdown/LanguageDropdown.tsx index 0cd0ccbe1..d48aa2e7b 100644 --- a/webclient/src/components/LanguageDropdown/LanguageDropdown.tsx +++ b/webclient/src/components/LanguageDropdown/LanguageDropdown.tsx @@ -4,8 +4,8 @@ import { useTranslation } from 'react-i18next'; import { Select, MenuItem } from '@mui/material'; import FormControl from '@mui/material/FormControl'; -import { Images } from 'images/Images'; -import { Language, LanguageCountry, LanguageNative } from 'types'; +import { Images } from '@app/images'; +import { App } from '@app/types'; import './LanguageDropdown.css'; @@ -26,19 +26,19 @@ const LanguageDropdown = () => { margin='dense' value={language} fullWidth={true} - onChange={e => setLanguage(e.target.value as Language)} + onChange={e => setLanguage(e.target.value as App.Language)} > { - Object.keys(Language).map((lang) => { - const country = LanguageCountry[lang]; + Object.keys(App.Language).map((lang) => { + const country = App.LanguageCountry[lang]; return (
- {LanguageNative[lang]} { - LanguageNative[lang] !== t(`Common.languages.${lang}`) && ( + {App.LanguageNative[lang]} { + App.LanguageNative[lang] !== t(`Common.languages.${lang}`) && ( <>({ t(`Common.languages.${lang}`) }) ) } diff --git a/webclient/src/components/Message/CardCallout.tsx b/webclient/src/components/Message/CardCallout.tsx index 9db660a30..3872ae17f 100644 --- a/webclient/src/components/Message/CardCallout.tsx +++ b/webclient/src/components/Message/CardCallout.tsx @@ -3,7 +3,7 @@ import React, { useMemo, useState } from 'react'; import { styled } from '@mui/material/styles'; import Popover from '@mui/material/Popover'; -import { CardDTO, TokenDTO } from 'services'; +import { CardDTO, TokenDTO } from '@app/services'; import CardDetails from '../CardDetails/CardDetails'; import TokenDetails from '../TokenDetails/TokenDetails'; diff --git a/webclient/src/components/Message/Message.tsx b/webclient/src/components/Message/Message.tsx index b7d433d18..bc9d640ec 100644 --- a/webclient/src/components/Message/Message.tsx +++ b/webclient/src/components/Message/Message.tsx @@ -3,14 +3,7 @@ import React, { useEffect, useMemo, useState } from 'react'; import { NavLink, generatePath } from 'react-router-dom'; -import { - RouteEnum, - URL_REGEX, - MESSAGE_SENDER_REGEX, - MENTION_REGEX, - CARD_CALLOUT_REGEX, - CALLOUT_BOUNDARY_REGEX, -} from 'types'; +import { App } from '@app/types'; import CardCallout from './CardCallout'; import './Message.css'; @@ -28,7 +21,7 @@ const ParsedMessage = ({ message }) => { const [name, setName] = useState(null); useMemo(() => { - const name = message.match(MESSAGE_SENDER_REGEX); + const name = message.match(App.MESSAGE_SENDER_REGEX); if (name) { setName(name[1]); @@ -46,29 +39,29 @@ const ParsedMessage = ({ message }) => { }; const PlayerLink = ({ name, label = name }) => ( - + {label} ); function parseMessage(message) { - return message.replace(MESSAGE_SENDER_REGEX, '') - .split(CARD_CALLOUT_REGEX) + return message.replace(App.MESSAGE_SENDER_REGEX, '') + .split(App.CARD_CALLOUT_REGEX) .filter(chunk => !!chunk) .map(parseChunks); } function parseChunks(chunk, index) { - if (chunk.match(CARD_CALLOUT_REGEX)) { - const name = chunk.replace(CALLOUT_BOUNDARY_REGEX, '').trim(); + if (chunk.match(App.CARD_CALLOUT_REGEX)) { + const name = chunk.replace(App.CALLOUT_BOUNDARY_REGEX, '').trim(); return (); } - if (chunk.match(URL_REGEX)) { + if (chunk.match(App.URL_REGEX)) { return parseUrlChunk(chunk); } - if (chunk.match(MENTION_REGEX)) { + if (chunk.match(App.MENTION_REGEX)) { return parseMentionChunk(chunk); } @@ -76,10 +69,10 @@ function parseChunks(chunk, index) { } function parseUrlChunk(chunk) { - return chunk.split(URL_REGEX) + return chunk.split(App.URL_REGEX) .filter(urlChunk => !!urlChunk) .map((urlChunk, index) => { - if (urlChunk.match(URL_REGEX)) { + if (urlChunk.match(App.URL_REGEX)) { return ({urlChunk}); } @@ -88,10 +81,10 @@ function parseUrlChunk(chunk) { } function parseMentionChunk(chunk) { - return chunk.split(MENTION_REGEX) + return chunk.split(App.MENTION_REGEX) .filter(mentionChunk => !!mentionChunk) .map((mentionChunk, index) => { - const mention = mentionChunk.match(MENTION_REGEX); + const mention = mentionChunk.match(App.MENTION_REGEX); if (mention) { const name = mention[0].substr(1); diff --git a/webclient/src/components/Token/Token.tsx b/webclient/src/components/Token/Token.tsx index 29b39ecc5..9a9b4768d 100644 --- a/webclient/src/components/Token/Token.tsx +++ b/webclient/src/components/Token/Token.tsx @@ -1,7 +1,7 @@ // eslint-disable-next-line import React, { useMemo, useState } from 'react'; -import { TokenDTO } from 'services'; +import { TokenDTO } from '@app/services'; import './Token.css'; diff --git a/webclient/src/components/TokenDetails/TokenDetails.tsx b/webclient/src/components/TokenDetails/TokenDetails.tsx index f3d8aed96..9166a554f 100644 --- a/webclient/src/components/TokenDetails/TokenDetails.tsx +++ b/webclient/src/components/TokenDetails/TokenDetails.tsx @@ -1,7 +1,7 @@ // eslint-disable-next-line import React, { useMemo, useState } from 'react'; -import { TokenDTO } from 'services'; +import { TokenDTO } from '@app/services'; import Token from '../Token/Token'; diff --git a/webclient/src/components/UserDisplay/UserDisplay.tsx b/webclient/src/components/UserDisplay/UserDisplay.tsx index 74a3acfe0..a19bc9912 100644 --- a/webclient/src/components/UserDisplay/UserDisplay.tsx +++ b/webclient/src/components/UserDisplay/UserDisplay.tsx @@ -5,12 +5,11 @@ import { NavLink, generatePath } from 'react-router-dom'; import Menu from '@mui/material/Menu'; import MenuItem from '@mui/material/MenuItem'; -import { Images } from 'images/Images'; -import { SessionService } from 'api'; -import { ServerSelectors } from 'store'; -import { RouteEnum } from 'types'; -import type { ServerInfo_User } from 'generated/proto/serverinfo_user_pb'; -import { useAppSelector } from 'store/store'; +import { Images } from '@app/images'; +import { SessionService } from '@app/api'; +import { ServerSelectors } from '@app/store'; +import { App, Data } from '@app/types'; +import { useAppSelector } from '@app/store'; import './UserDisplay.css'; @@ -51,7 +50,7 @@ const UserDisplay = ({ user }: UserDisplayProps) => { return (
- +
{country}
{name}
@@ -68,7 +67,7 @@ const UserDisplay = ({ user }: UserDisplayProps) => { : undefined } > - + Chat { @@ -88,7 +87,7 @@ const UserDisplay = ({ user }: UserDisplayProps) => { }; interface UserDisplayProps { - user: ServerInfo_User; + user: Data.ServerInfo_User; } export default UserDisplay; diff --git a/webclient/src/components/index.ts b/webclient/src/components/index.ts index 2d67b3003..5337b3b10 100644 --- a/webclient/src/components/index.ts +++ b/webclient/src/components/index.ts @@ -17,3 +17,6 @@ export { default as ScrollToBottomOnChanges } from './ScrollToBottomOnChanges/Sc // Guards export { default as AuthGuard } from './Guard/AuthGuard'; export { default as ModGuard } from './Guard/ModGuard'; + +// Toast +export { default as Toast, useToast, ToastProvider } from './Toast'; diff --git a/webclient/src/containers/Account/Account.tsx b/webclient/src/containers/Account/Account.tsx index e25f59b72..8e0088d06 100644 --- a/webclient/src/containers/Account/Account.tsx +++ b/webclient/src/containers/Account/Account.tsx @@ -6,11 +6,11 @@ import Button from '@mui/material/Button'; import ListItemButton from '@mui/material/ListItemButton'; import Paper from '@mui/material/Paper'; -import { UserDisplay, VirtualList, AuthGuard, LanguageDropdown } from 'components'; -import { AuthenticationService, SessionService } from 'api'; -import { ServerSelectors } from 'store'; -import Layout from 'containers/Layout/Layout'; -import { useAppSelector } from 'store/store'; +import { UserDisplay, VirtualList, AuthGuard, LanguageDropdown } from '@app/components'; +import { AuthenticationService, SessionService } from '@app/api'; +import { ServerSelectors } from '@app/store'; +import Layout from '../Layout/Layout'; +import { useAppSelector } from '@app/store'; import AddToBuddies from './AddToBuddies'; import AddToIgnore from './AddToIgnore'; diff --git a/webclient/src/containers/Account/AddToBuddies.tsx b/webclient/src/containers/Account/AddToBuddies.tsx index 3aa5489ad..7fb05859d 100644 --- a/webclient/src/containers/Account/AddToBuddies.tsx +++ b/webclient/src/containers/Account/AddToBuddies.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Form } from 'react-final-form' -import { InputAction } from 'components'; +import { InputAction } from '@app/components'; const AddToBuddies = ({ onSubmit }) => (
onSubmit(values)}> diff --git a/webclient/src/containers/Account/AddToIgnore.tsx b/webclient/src/containers/Account/AddToIgnore.tsx index 270036946..5149de0f5 100644 --- a/webclient/src/containers/Account/AddToIgnore.tsx +++ b/webclient/src/containers/Account/AddToIgnore.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Form } from 'react-final-form' -import { InputAction } from 'components'; +import { InputAction } from '@app/components'; const AddToIgnore = ({ onSubmit }) => ( onSubmit(values)}> diff --git a/webclient/src/containers/App/AppShell.tsx b/webclient/src/containers/App/AppShell.tsx index c28d20067..9476d76cd 100644 --- a/webclient/src/containers/App/AppShell.tsx +++ b/webclient/src/containers/App/AppShell.tsx @@ -2,13 +2,13 @@ import { Component, Suspense } from 'react'; import { Provider } from 'react-redux'; import { MemoryRouter as Router } from 'react-router-dom'; import CssBaseline from '@mui/material/CssBaseline'; -import { store } from 'store'; +import { store } from '@app/store'; import Routes from './AppShellRoutes'; import FeatureDetection from './FeatureDetection'; import './AppShell.css'; -import { ToastProvider } from 'components/Toast' +import { ToastProvider } from '@app/components' class AppShell extends Component { componentDidMount() { diff --git a/webclient/src/containers/App/AppShellRoutes.tsx b/webclient/src/containers/App/AppShellRoutes.tsx index b07961eef..fc097ffd4 100644 --- a/webclient/src/containers/App/AppShellRoutes.tsx +++ b/webclient/src/containers/App/AppShellRoutes.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Route, Routes } from 'react-router-dom'; -import { RouteEnum } from 'types'; +import { App } from '@app/types'; import { Account, Decks, @@ -13,22 +13,22 @@ import { Logs, Initialize, Unsupported -} from 'containers'; +} from '..'; const AppShellRoutes = () => (
} /> - } /> - } /> - } /> - } /> - } /> - {} />} - } /> - } /> - } /> + } /> + } /> + } /> + } /> + } /> + {} />} + } /> + } /> + } />
); diff --git a/webclient/src/containers/App/FeatureDetection.tsx b/webclient/src/containers/App/FeatureDetection.tsx index d7ef75aeb..8ac37f321 100644 --- a/webclient/src/containers/App/FeatureDetection.tsx +++ b/webclient/src/containers/App/FeatureDetection.tsx @@ -1,7 +1,7 @@ import { useEffect, useState } from 'react'; import { Navigate } from 'react-router-dom'; -import { dexieService } from 'services'; -import { RouteEnum } from 'types'; +import { dexieService } from '@app/services'; +import { App } from '@app/types'; const FeatureDetection = () => { const [unsupported, setUnsupported] = useState(false); @@ -15,7 +15,7 @@ const FeatureDetection = () => { }, []); return unsupported - ? + ? : <>; function detectIndexedDB() { diff --git a/webclient/src/containers/Decks/Decks.tsx b/webclient/src/containers/Decks/Decks.tsx index 190196dc9..f37d67261 100644 --- a/webclient/src/containers/Decks/Decks.tsx +++ b/webclient/src/containers/Decks/Decks.tsx @@ -1,8 +1,8 @@ // eslint-disable-next-line import React, { Component } from "react"; -import { AuthGuard } from 'components/index'; -import Layout from 'containers/Layout/Layout'; +import { AuthGuard } from '@app/components'; +import Layout from '../Layout/Layout'; import './Decks.css'; diff --git a/webclient/src/containers/Game/Game.tsx b/webclient/src/containers/Game/Game.tsx index 694ffb46d..6be3ab1ca 100644 --- a/webclient/src/containers/Game/Game.tsx +++ b/webclient/src/containers/Game/Game.tsx @@ -1,8 +1,8 @@ // eslint-disable-next-line import React, { Component } from "react"; -import { AuthGuard } from 'components'; -import Layout from 'containers/Layout/Layout'; +import { AuthGuard } from '@app/components'; +import Layout from '../Layout/Layout'; import './Game.css'; diff --git a/webclient/src/containers/Initialize/Initialize.tsx b/webclient/src/containers/Initialize/Initialize.tsx index 92f8223e7..9e32c1925 100644 --- a/webclient/src/containers/Initialize/Initialize.tsx +++ b/webclient/src/containers/Initialize/Initialize.tsx @@ -3,11 +3,11 @@ import { useTranslation, Trans } from 'react-i18next'; import { Navigate } from 'react-router-dom'; import Typography from '@mui/material/Typography'; -import { Images } from 'images'; -import { ServerSelectors } from 'store'; -import { RouteEnum } from 'types'; -import Layout from 'containers/Layout/Layout'; -import { useAppSelector } from 'store/store'; +import { Images } from '@app/images'; +import { ServerSelectors } from '@app/store'; +import { App } from '@app/types'; +import Layout from '../Layout/Layout'; +import { useAppSelector } from '@app/store'; import './Initialize.css'; @@ -34,7 +34,7 @@ const Initialize = () => { const { t } = useTranslation(); return initialized - ? + ? : ( diff --git a/webclient/src/containers/Layout/LeftNav.tsx b/webclient/src/containers/Layout/LeftNav.tsx index 196087d7a..db4227b36 100644 --- a/webclient/src/containers/Layout/LeftNav.tsx +++ b/webclient/src/containers/Layout/LeftNav.tsx @@ -8,12 +8,12 @@ import CloseIcon from '@mui/icons-material/Close'; import MailOutlineRoundedIcon from '@mui/icons-material/MailOutline'; import MenuRoundedIcon from '@mui/icons-material/MenuRounded'; -import { AuthenticationService, RoomsService } from 'api'; -import { CardImportDialog } from 'dialogs'; -import { Images } from 'images'; -import { RoomsSelectors, ServerSelectors } from 'store'; -import { RouteEnum } from 'types'; -import { useAppSelector } from 'store/store'; +import { AuthenticationService, RoomsService } from '@app/api'; +import { CardImportDialog } from '@app/dialogs'; +import { Images } from '@app/images'; +import { RoomsSelectors, ServerSelectors } from '@app/store'; +import { App } from '@app/types'; +import { useAppSelector } from '@app/store'; import './LeftNav.css'; @@ -82,7 +82,7 @@ const LeftNav = () => {
- + logo { AuthenticationService.isConnected(serverState) && ( @@ -98,8 +98,8 @@ const LeftNav = () => { className="LeftNav-nav__link-btn" to={ joinedRooms.length - ? generatePath(RouteEnum.ROOM, { roomId: joinedRooms[0].roomId.toString() }) - : RouteEnum.SERVER + ? generatePath(App.RouteEnum.ROOM, { roomId: joinedRooms[0].roomId.toString() }) + : App.RouteEnum.SERVER } > Rooms @@ -108,7 +108,9 @@ const LeftNav = () => {
{joinedRooms.map(({ name, roomId }) => (
- + {name} leaveRoom(event, roomId)}> @@ -120,13 +122,13 @@ const LeftNav = () => {
- + Games
- + Decks diff --git a/webclient/src/containers/Login/Login.tsx b/webclient/src/containers/Login/Login.tsx index d15332564..d0668fa11 100644 --- a/webclient/src/containers/Login/Login.tsx +++ b/webclient/src/containers/Login/Login.tsx @@ -6,20 +6,20 @@ import Button from '@mui/material/Button'; import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; -import { AuthenticationService } from 'api'; -import { RegistrationDialog, RequestPasswordResetDialog, ResetPasswordDialog, AccountActivationDialog } from 'dialogs'; -import { LanguageDropdown } from 'components'; -import { LoginForm } from 'forms'; -import { useReduxEffect, useFireOnce } from 'hooks'; -import { Images } from 'images'; -import { HostDTO, serverProps } from 'services'; -import { RouteEnum, WebSocketConnectOptions, getHostPort } from 'types'; -import { ServerSelectors, ServerTypes } from 'store'; -import Layout from 'containers/Layout/Layout'; -import { useAppSelector } from 'store/store'; +import { AuthenticationService } from '@app/api'; +import { RegistrationDialog, RequestPasswordResetDialog, ResetPasswordDialog, AccountActivationDialog } from '@app/dialogs'; +import { LanguageDropdown } from '@app/components'; +import { LoginForm } from '@app/forms'; +import { useReduxEffect, useFireOnce } from '@app/hooks'; +import { Images } from '@app/images'; +import { HostDTO, serverProps } from '@app/services'; +import { App, Enriched } from '@app/types'; +import { ServerSelectors, ServerTypes } from '@app/store'; +import Layout from '../Layout/Layout'; +import { useAppSelector } from '@app/store'; import './Login.css'; -import { useToast } from 'components/Toast'; +import { useToast } from '@app/components'; const PREFIX = 'Login'; @@ -71,7 +71,7 @@ const Login = () => { const isConnected = AuthenticationService.isConnected(state); - const [pendingActivationOptions, setPendingActivationOptions] = useState(null); + const [pendingActivationOptions, setPendingActivationOptions] = useState(null); const [rememberLogin, setRememberLogin] = useState(null); const [dialogState, setDialogState] = useState({ @@ -126,17 +126,17 @@ const Login = () => { setRememberLogin(loginForm); const { userName, password, selectedHost, remember } = loginForm; - const options: WebSocketConnectOptions = { - ...getHostPort(selectedHost), + const options: Omit = { + ...App.getHostPort(selectedHost), userName, - password + password, }; if (remember && !password) { options.hashedPassword = selectedHost.hashedPassword; } - AuthenticationService.login(options as WebSocketConnectOptions); + AuthenticationService.login(options); }, []); const [submitButtonDisabled, resetSubmitButton, handleLogin] = useFireOnce(onSubmitLogin); @@ -156,7 +156,7 @@ const Login = () => { const { userName, password, email, country, realName, selectedHost } = registerForm; AuthenticationService.register({ - ...getHostPort(selectedHost), + ...App.getHostPort(selectedHost), userName, password, email, @@ -166,15 +166,20 @@ const Login = () => { }; const handleAccountActivationDialogSubmit = ({ token }) => { + if (!pendingActivationOptions) { + return; + } AuthenticationService.activateAccount({ - ...pendingActivationOptions, + host: pendingActivationOptions.host, + port: pendingActivationOptions.port, + userName: pendingActivationOptions.userName, token, }); }; const handleRequestPasswordResetDialogSubmit = (form) => { const { userName, email, selectedHost } = form; - const { host, port } = getHostPort(selectedHost); + const { host, port } = App.getHostPort(selectedHost); if (email) { AuthenticationService.resetPasswordChallenge({ userName, email, host, port }); @@ -185,7 +190,7 @@ const Login = () => { }; const handleResetPasswordDialogSubmit = ({ userName, token, newPassword, selectedHost }) => { - const { host, port } = getHostPort(selectedHost); + const { host, port } = App.getHostPort(selectedHost); AuthenticationService.resetPassword({ userName, token, newPassword, host, port }); }; @@ -234,7 +239,7 @@ const Login = () => { return ( - { isConnected && } + { isConnected && }
diff --git a/webclient/src/containers/Logs/Logs.tsx b/webclient/src/containers/Logs/Logs.tsx index 37fa4d27a..317a04d14 100644 --- a/webclient/src/containers/Logs/Logs.tsx +++ b/webclient/src/containers/Logs/Logs.tsx @@ -2,12 +2,12 @@ import React, { useEffect } from "react"; import * as _ from 'lodash'; -import { ModeratorService } from 'api'; -import { AuthGuard, ModGuard } from 'components'; -import { SearchForm } from 'forms'; -import { ServerDispatch, ServerSelectors } from 'store'; -import { LogFilters } from 'types'; -import { useAppSelector } from 'store/store'; +import { ModeratorService } from '@app/api'; +import { AuthGuard, ModGuard } from '@app/components'; +import { SearchForm } from '@app/forms'; +import { ServerDispatch, ServerSelectors } from '@app/store'; +import { Data } from '@app/types'; +import { useAppSelector } from '@app/store'; import LogResults from './LogResults'; import './Logs.css'; @@ -43,7 +43,7 @@ const Logs = () => { }, []); }; - const onSubmit = (fields: LogFilters) => { + const onSubmit = (fields: Data.ViewLogHistoryParams) => { const trimmedFields: any = trimFields(fields); const { userName, ipAddress, gameName, gameId, message, logLocation } = trimmedFields; diff --git a/webclient/src/containers/Player/Player.tsx b/webclient/src/containers/Player/Player.tsx index 74feff8bd..58f5403cf 100644 --- a/webclient/src/containers/Player/Player.tsx +++ b/webclient/src/containers/Player/Player.tsx @@ -1,8 +1,8 @@ // eslint-disable-next-line import React, { Component } from "react"; -import Layout from 'containers/Layout/Layout'; +import Layout from '../Layout/Layout'; -import { AuthGuard } from 'components'; +import { AuthGuard } from '@app/components'; class Player extends Component { render() { diff --git a/webclient/src/containers/Room/Games.tsx b/webclient/src/containers/Room/Games.tsx index f2489a29f..992170c4e 100644 --- a/webclient/src/containers/Room/Games.tsx +++ b/webclient/src/containers/Room/Games.tsx @@ -12,9 +12,9 @@ import Tooltip from '@mui/material/Tooltip'; // import { RoomsService } from "AppShell/common/services"; -import { SortUtil, RoomsDispatch, RoomsSelectors } from 'store'; -import { UserDisplay } from 'components'; -import { useAppSelector } from 'store/store'; +import { SortUtil, RoomsDispatch, RoomsSelectors } from '@app/store'; +import { UserDisplay } from '@app/components'; +import { useAppSelector } from '@app/store'; import './Games.css'; diff --git a/webclient/src/containers/Room/Messages.tsx b/webclient/src/containers/Room/Messages.tsx index 81aa671b8..88e727b53 100644 --- a/webclient/src/containers/Room/Messages.tsx +++ b/webclient/src/containers/Room/Messages.tsx @@ -1,7 +1,7 @@ // eslint-disable-next-line import React from "react"; -import { Message } from 'components'; +import { Message } from '@app/components'; import './Messages.css'; diff --git a/webclient/src/containers/Room/OpenGames.tsx b/webclient/src/containers/Room/OpenGames.tsx index 79c2a855d..d18d61b9f 100644 --- a/webclient/src/containers/Room/OpenGames.tsx +++ b/webclient/src/containers/Room/OpenGames.tsx @@ -12,9 +12,9 @@ import Tooltip from '@mui/material/Tooltip'; // import { RoomsService } from "AppShell/common/services"; -import { SortUtil, RoomsDispatch, RoomsSelectors } from 'store'; -import { UserDisplay } from 'components'; -import { useAppSelector } from 'store/store'; +import { SortUtil, RoomsDispatch, RoomsSelectors } from '@app/store'; +import { UserDisplay } from '@app/components'; +import { useAppSelector } from '@app/store'; import './OpenGames.css'; diff --git a/webclient/src/containers/Room/Room.tsx b/webclient/src/containers/Room/Room.tsx index 4c470d0fa..15821d200 100644 --- a/webclient/src/containers/Room/Room.tsx +++ b/webclient/src/containers/Room/Room.tsx @@ -4,12 +4,12 @@ import { useNavigate, useParams, generatePath } from 'react-router-dom'; import ListItemButton from '@mui/material/ListItemButton'; import Paper from '@mui/material/Paper'; -import { RoomsService } from 'api'; -import { ScrollToBottomOnChanges, ThreePaneLayout, UserDisplay, VirtualList, AuthGuard } from 'components'; -import { RoomsSelectors } from 'store'; -import { useAppSelector } from 'store/store'; -import { RouteEnum } from 'types'; -import Layout from 'containers/Layout/Layout'; +import { RoomsService } from '@app/api'; +import { ScrollToBottomOnChanges, ThreePaneLayout, UserDisplay, VirtualList, AuthGuard } from '@app/components'; +import { RoomsSelectors } from '@app/store'; +import { useAppSelector } from '@app/store'; +import { App } from '@app/types'; +import Layout from '../Layout/Layout'; import OpenGames from './OpenGames'; import Messages from './Messages'; @@ -32,7 +32,7 @@ const Room = () => { useEffect(() => { if (!joined.find(({ roomId: id }) => id === roomId)) { - navigate(generatePath(RouteEnum.SERVER)); + navigate(generatePath(App.RouteEnum.SERVER)); } }, [joined]); diff --git a/webclient/src/containers/Room/SayMessage.tsx b/webclient/src/containers/Room/SayMessage.tsx index 4dd62dfb4..589ebdacc 100644 --- a/webclient/src/containers/Room/SayMessage.tsx +++ b/webclient/src/containers/Room/SayMessage.tsx @@ -1,7 +1,7 @@ import React from 'react'; import { Form } from 'react-final-form' -import { InputAction } from 'components'; +import { InputAction } from '@app/components'; const SayMessage = ({ onSubmit }) => ( diff --git a/webclient/src/containers/Server/Rooms.tsx b/webclient/src/containers/Server/Rooms.tsx index 517f24380..c4705cc3f 100644 --- a/webclient/src/containers/Server/Rooms.tsx +++ b/webclient/src/containers/Server/Rooms.tsx @@ -11,8 +11,8 @@ import TableHead from '@mui/material/TableHead'; import TableRow from '@mui/material/TableRow'; -import { RoomsService } from 'api'; -import { RouteEnum } from 'types'; +import { RoomsService } from '@app/api'; +import { App } from '@app/types'; import './Rooms.css'; @@ -21,7 +21,7 @@ const Rooms = ({ rooms, joinedRooms }) => { function onClick(roomId) { if (_.find(joinedRooms, room => room.roomId === roomId)) { - navigate(generatePath(RouteEnum.ROOM, { roomId })); + navigate(generatePath(App.RouteEnum.ROOM, { roomId })); } else { RoomsService.joinRoom(roomId); } diff --git a/webclient/src/containers/Server/Server.tsx b/webclient/src/containers/Server/Server.tsx index 3d1778343..45ac959f9 100644 --- a/webclient/src/containers/Server/Server.tsx +++ b/webclient/src/containers/Server/Server.tsx @@ -5,13 +5,13 @@ import { generatePath, useNavigate } from 'react-router-dom'; import ListItemButton from '@mui/material/ListItemButton'; import Paper from '@mui/material/Paper'; -import { AuthGuard, ThreePaneLayout, UserDisplay, VirtualList } from 'components'; -import { useReduxEffect } from 'hooks'; -import { RoomsSelectors, RoomsTypes, ServerSelectors } from 'store'; -import { RouteEnum } from 'types'; -import { useAppSelector } from 'store/store'; +import { AuthGuard, ThreePaneLayout, UserDisplay, VirtualList } from '@app/components'; +import { useReduxEffect } from '@app/hooks'; +import { RoomsSelectors, RoomsTypes, ServerSelectors } from '@app/store'; +import { App } from '@app/types'; +import { useAppSelector } from '@app/store'; import Rooms from './Rooms'; -import Layout from 'containers/Layout/Layout'; +import Layout from '../Layout/Layout'; import './Server.css'; @@ -24,7 +24,7 @@ const Server = () => { useReduxEffect((action: any) => { const roomId = action.roomInfo.roomId.toString(); - navigate(generatePath(RouteEnum.ROOM, { roomId })); + navigate(generatePath(App.RouteEnum.ROOM, { roomId })); }, RoomsTypes.JOIN_ROOM, []); return ( diff --git a/webclient/src/containers/Unsupported/Unsupported.tsx b/webclient/src/containers/Unsupported/Unsupported.tsx index a4eec9d72..5e491d17a 100644 --- a/webclient/src/containers/Unsupported/Unsupported.tsx +++ b/webclient/src/containers/Unsupported/Unsupported.tsx @@ -1,7 +1,7 @@ import { useTranslation } from 'react-i18next'; import Paper from '@mui/material/Paper'; import Typography from '@mui/material/Typography'; -import Layout from 'containers/Layout/Layout'; +import Layout from '../Layout/Layout'; import './Unsupported.css'; diff --git a/webclient/src/dialogs/AccountActivationDialog/AccountActivationDialog.tsx b/webclient/src/dialogs/AccountActivationDialog/AccountActivationDialog.tsx index b14383a20..476a399ec 100644 --- a/webclient/src/dialogs/AccountActivationDialog/AccountActivationDialog.tsx +++ b/webclient/src/dialogs/AccountActivationDialog/AccountActivationDialog.tsx @@ -7,7 +7,7 @@ import CloseIcon from '@mui/icons-material/Close'; import Typography from '@mui/material/Typography'; import { useTranslation } from 'react-i18next'; -import { AccountActivationForm } from 'forms'; +import { AccountActivationForm } from '@app/forms'; import './AccountActivationDialog.css'; diff --git a/webclient/src/dialogs/CardImportDialog/CardImportDialog.tsx b/webclient/src/dialogs/CardImportDialog/CardImportDialog.tsx index 1293fa5ba..a5fc1da90 100644 --- a/webclient/src/dialogs/CardImportDialog/CardImportDialog.tsx +++ b/webclient/src/dialogs/CardImportDialog/CardImportDialog.tsx @@ -6,7 +6,7 @@ import IconButton from '@mui/material/IconButton'; import CloseIcon from '@mui/icons-material/Close'; import Typography from '@mui/material/Typography'; -import { CardImportForm } from 'forms'; +import { CardImportForm } from '@app/forms'; import './CardImportDialog.css'; diff --git a/webclient/src/dialogs/KnownHostDialog/KnownHostDialog.tsx b/webclient/src/dialogs/KnownHostDialog/KnownHostDialog.tsx index bfc164e0d..193b0facd 100644 --- a/webclient/src/dialogs/KnownHostDialog/KnownHostDialog.tsx +++ b/webclient/src/dialogs/KnownHostDialog/KnownHostDialog.tsx @@ -8,7 +8,7 @@ import CloseIcon from '@mui/icons-material/Close'; import Typography from '@mui/material/Typography'; import { useTranslation } from 'react-i18next'; -import { KnownHostForm } from 'forms'; +import { KnownHostForm } from '@app/forms'; import './KnownHostDialog.css'; diff --git a/webclient/src/dialogs/RegistrationDialog/RegistrationDialog.tsx b/webclient/src/dialogs/RegistrationDialog/RegistrationDialog.tsx index 098306077..a4ab8aef5 100644 --- a/webclient/src/dialogs/RegistrationDialog/RegistrationDialog.tsx +++ b/webclient/src/dialogs/RegistrationDialog/RegistrationDialog.tsx @@ -7,7 +7,7 @@ import CloseIcon from '@mui/icons-material/Close'; import Typography from '@mui/material/Typography'; import { useTranslation } from 'react-i18next'; -import { RegisterForm } from 'forms'; +import { RegisterForm } from '@app/forms'; import './RegistrationDialog.css'; diff --git a/webclient/src/dialogs/RequestPasswordResetDialog/RequestPasswordResetDialog.tsx b/webclient/src/dialogs/RequestPasswordResetDialog/RequestPasswordResetDialog.tsx index 6b0158314..96c7b82a8 100644 --- a/webclient/src/dialogs/RequestPasswordResetDialog/RequestPasswordResetDialog.tsx +++ b/webclient/src/dialogs/RequestPasswordResetDialog/RequestPasswordResetDialog.tsx @@ -7,7 +7,7 @@ import CloseIcon from '@mui/icons-material/Close'; import Typography from '@mui/material/Typography'; import { useTranslation } from 'react-i18next'; -import { RequestPasswordResetForm } from 'forms'; +import { RequestPasswordResetForm } from '@app/forms'; import './RequestPasswordResetDialog.css'; diff --git a/webclient/src/dialogs/ResetPasswordDialog/ResetPasswordDialog.tsx b/webclient/src/dialogs/ResetPasswordDialog/ResetPasswordDialog.tsx index f4e1cc520..5c77aaef5 100644 --- a/webclient/src/dialogs/ResetPasswordDialog/ResetPasswordDialog.tsx +++ b/webclient/src/dialogs/ResetPasswordDialog/ResetPasswordDialog.tsx @@ -7,7 +7,7 @@ import CloseIcon from '@mui/icons-material/Close'; import Typography from '@mui/material/Typography'; import { useTranslation } from 'react-i18next'; -import { ResetPasswordForm } from 'forms'; +import { ResetPasswordForm } from '@app/forms'; import './ResetPasswordDialog.css'; diff --git a/webclient/src/forms/AccountActivationForm/AccountActivationForm.tsx b/webclient/src/forms/AccountActivationForm/AccountActivationForm.tsx index d2ec8e662..4ffc9e51a 100644 --- a/webclient/src/forms/AccountActivationForm/AccountActivationForm.tsx +++ b/webclient/src/forms/AccountActivationForm/AccountActivationForm.tsx @@ -6,9 +6,9 @@ import { useTranslation } from 'react-i18next'; import Button from '@mui/material/Button'; import Typography from '@mui/material/Typography'; -import { InputField } from 'components'; -import { useReduxEffect } from 'hooks'; -import { ServerTypes } from 'store'; +import { InputField } from '@app/components'; +import { useReduxEffect } from '@app/hooks'; +import { ServerTypes } from '@app/store'; import './AccountActivationForm.css'; diff --git a/webclient/src/forms/CardImportForm/CardImportForm.tsx b/webclient/src/forms/CardImportForm/CardImportForm.tsx index 38a1893cb..ab959ea99 100644 --- a/webclient/src/forms/CardImportForm/CardImportForm.tsx +++ b/webclient/src/forms/CardImportForm/CardImportForm.tsx @@ -8,8 +8,8 @@ import Step from '@mui/material/Step'; import StepLabel from '@mui/material/StepLabel'; import CircularProgress from '@mui/material/CircularProgress'; -import { InputField, VirtualList } from 'components'; -import { cardImporterService, CardDTO, SetDTO, TokenDTO } from 'services'; +import { InputField, VirtualList } from '@app/components'; +import { cardImporterService, CardDTO, SetDTO, TokenDTO } from '@app/services'; import './CardImportForm.css'; diff --git a/webclient/src/forms/KnownHostForm/KnownHostForm.tsx b/webclient/src/forms/KnownHostForm/KnownHostForm.tsx index 5fd31e13f..86dc4f85e 100644 --- a/webclient/src/forms/KnownHostForm/KnownHostForm.tsx +++ b/webclient/src/forms/KnownHostForm/KnownHostForm.tsx @@ -6,7 +6,7 @@ import { useTranslation } from 'react-i18next'; import Button from '@mui/material/Button'; import AnchorLink from '@mui/material/Link'; -import { InputField } from 'components'; +import { InputField } from '@app/components'; import './KnownHostForm.css'; diff --git a/webclient/src/forms/LoginForm/LoginForm.tsx b/webclient/src/forms/LoginForm/LoginForm.tsx index 20c819304..4d6ee4e37 100644 --- a/webclient/src/forms/LoginForm/LoginForm.tsx +++ b/webclient/src/forms/LoginForm/LoginForm.tsx @@ -5,12 +5,11 @@ import { useTranslation } from 'react-i18next'; import Button from '@mui/material/Button'; -import { CheckboxField, InputField, KnownHosts } from 'components'; -import { useAutoConnect } from 'hooks'; -import { HostDTO, SettingDTO } from 'services'; -import { APP_USER } from 'types'; -import { useAppSelector } from 'store'; -import { Selectors as ServerSelectors } from 'store/server'; +import { CheckboxField, InputField, KnownHosts } from '@app/components'; +import { useAutoConnect } from '@app/hooks'; +import { HostDTO, SettingDTO } from '@app/services'; +import { App } from '@app/types'; +import { useAppSelector, ServerSelectors } from '@app/store'; import './LoginForm.css'; @@ -55,7 +54,7 @@ const LoginForm = ({ onSubmit, disableSubmitButton, onResetPassword }: LoginForm const { values } = form.getState(); useEffect(() => { - SettingDTO.get(APP_USER).then((userSetting: SettingDTO) => { + SettingDTO.get(App.APP_USER).then((userSetting: SettingDTO) => { if (userSetting?.autoConnect && !connectionAttemptMade) { HostDTO.getAll().then(hosts => { let lastSelectedHost = hosts.find(({ lastSelected }) => lastSelected); diff --git a/webclient/src/forms/RegisterForm/RegisterForm.tsx b/webclient/src/forms/RegisterForm/RegisterForm.tsx index 42b34fda0..b87d0ebf3 100644 --- a/webclient/src/forms/RegisterForm/RegisterForm.tsx +++ b/webclient/src/forms/RegisterForm/RegisterForm.tsx @@ -8,12 +8,12 @@ import { useTranslation } from 'react-i18next'; import Button from '@mui/material/Button'; import Typography from '@mui/material/Typography'; -import { CountryDropdown, InputField, KnownHosts } from 'components'; -import { useReduxEffect } from 'hooks'; -import { ServerDispatch, ServerSelectors, ServerTypes } from 'store'; +import { CountryDropdown, InputField, KnownHosts } from '@app/components'; +import { useReduxEffect } from '@app/hooks'; +import { ServerDispatch, ServerSelectors, ServerTypes } from '@app/store'; import './RegisterForm.css'; -import { useToast } from 'components/Toast'; +import { useToast } from '@app/components'; const RegisterForm = ({ onSubmit }: RegisterFormProps) => { const { t } = useTranslation(); diff --git a/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.tsx b/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.tsx index 589f8da1e..46776cd37 100644 --- a/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.tsx +++ b/webclient/src/forms/RequestPasswordResetForm/RequestPasswordResetForm.tsx @@ -7,9 +7,9 @@ import { useTranslation } from 'react-i18next'; import Button from '@mui/material/Button'; import Typography from '@mui/material/Typography'; -import { InputField, KnownHosts } from 'components'; -import { useReduxEffect } from 'hooks'; -import { ServerTypes } from 'store'; +import { InputField, KnownHosts } from '@app/components'; +import { useReduxEffect } from '@app/hooks'; +import { ServerTypes } from '@app/store'; import './RequestPasswordResetForm.css'; diff --git a/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.tsx b/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.tsx index f0440363f..9e9984edf 100644 --- a/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.tsx +++ b/webclient/src/forms/ResetPasswordForm/ResetPasswordForm.tsx @@ -6,9 +6,9 @@ import { useTranslation } from 'react-i18next'; import Button from '@mui/material/Button'; import Typography from '@mui/material/Typography'; -import { InputField, KnownHosts } from 'components'; -import { useReduxEffect } from '../../hooks'; -import { ServerTypes } from '../../store'; +import { InputField, KnownHosts } from '@app/components'; +import { useReduxEffect } from '@app/hooks'; +import { ServerTypes } from '@app/store'; import './ResetPasswordForm.css'; diff --git a/webclient/src/forms/SearchForm/SearchForm.tsx b/webclient/src/forms/SearchForm/SearchForm.tsx index bca8a498f..07b1792ad 100644 --- a/webclient/src/forms/SearchForm/SearchForm.tsx +++ b/webclient/src/forms/SearchForm/SearchForm.tsx @@ -6,7 +6,7 @@ import Button from '@mui/material/Button'; import Divider from '@mui/material/Divider'; import Paper from '@mui/material/Paper'; -import { InputField, CheckboxField } from 'components'; +import { InputField, CheckboxField } from '@app/components'; import './SearchForm.css'; diff --git a/webclient/src/hooks/useAutoConnect.ts b/webclient/src/hooks/useAutoConnect.ts index 6a2690975..f8daa78aa 100644 --- a/webclient/src/hooks/useAutoConnect.ts +++ b/webclient/src/hooks/useAutoConnect.ts @@ -1,16 +1,16 @@ import { useEffect, useState } from 'react'; -import { SettingDTO } from 'services'; -import { APP_USER } from 'types'; +import { SettingDTO } from '@app/services'; +import { App } from '@app/types'; export function useAutoConnect() { const [setting, setSetting] = useState(undefined); const [autoConnect, setAutoConnect] = useState(undefined); useEffect(() => { - SettingDTO.get(APP_USER).then((setting: SettingDTO) => { + SettingDTO.get(App.APP_USER).then((setting: SettingDTO) => { if (!setting) { - setting = new SettingDTO(APP_USER); + setting = new SettingDTO(App.APP_USER); setting.save(); } diff --git a/webclient/src/i18n-backend.ts b/webclient/src/i18n-backend.ts index 0a92a3a0c..9c481f5eb 100644 --- a/webclient/src/i18n-backend.ts +++ b/webclient/src/i18n-backend.ts @@ -1,18 +1,18 @@ import { ModuleType } from 'i18next'; -import { Language } from 'types'; +import { App } from '@app/types'; class I18nBackend { static type: ModuleType = 'backend'; static BASE_URL = `${import.meta.env.BASE_URL}locales`; read(language, namespace, callback) { - if (!Language[language]) { + if (!language[App.Language]) { callback(true, null); return; } - fetch(`${I18nBackend.BASE_URL}/${Language[language]}/${namespace}.json`) + fetch(`${I18nBackend.BASE_URL}/${language[App.Language]}/${namespace}.json`) .then(resp => resp.json().then(json => callback(null, json))) .catch(error => callback(error, null)); } diff --git a/webclient/src/i18n.ts b/webclient/src/i18n.ts index ef1885965..0c37911f5 100644 --- a/webclient/src/i18n.ts +++ b/webclient/src/i18n.ts @@ -3,7 +3,7 @@ import LanguageDetector from 'i18next-browser-languagedetector'; import ICU from 'i18next-icu'; import { initReactI18next } from 'react-i18next'; -import { Language } from 'types'; +import { App } from '@app/types'; import I18nBackend from './i18n-backend'; @@ -17,9 +17,9 @@ i18n .use(initReactI18next) // for all options read: https://www.i18next.com/overview/configuration-options .init({ - fallbackLng: Language['en-US'], + fallbackLng: App.Language['en-US'], resources: { - [Language['en-US']]: { translation }, + [App.Language['en-US']]: { translation }, }, partialBundledLanguages: true, diff --git a/webclient/src/index.tsx b/webclient/src/index.tsx index 959d2adc7..ffebe2a70 100644 --- a/webclient/src/index.tsx +++ b/webclient/src/index.tsx @@ -1,9 +1,13 @@ +// MUST be first: installs BigInt.prototype.toJSON before any module that +// creates the Redux store or connects to Redux DevTools. +import './polyfills'; + import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; import { StyledEngineProvider } from '@mui/material'; import { ThemeProvider } from '@mui/material/styles'; -import { AppShell } from './containers'; +import { AppShell } from '@app/containers'; import { materialTheme } from './material-theme'; import './i18n'; diff --git a/webclient/src/polyfills.ts b/webclient/src/polyfills.ts new file mode 100644 index 000000000..d18a0c94c --- /dev/null +++ b/webclient/src/polyfills.ts @@ -0,0 +1,19 @@ +// Runtime polyfills that must execute before any other application module. +// Import this file first from `src/index.tsx`. + +// ── BigInt.prototype.toJSON ─────────────────────────────────────────────────── +// Protobuf-ES maps proto `int64`/`uint64` fields to native `BigInt`. Those +// land in Redux state (e.g. `ServerInfo_User.accountageSecs`, +// `Response_Register.deniedEndTime`, the outbound `cmdId`), and any consumer +// that JSON-stringifies state — notably the Redux DevTools browser +// extension, but also logging and error-boundary dumps — throws with +// "Do not know how to serialize a BigInt" because `BigInt.prototype` has no +// `toJSON`. Installing one globally makes `JSON.stringify` coerce +// `BigInt → string` instead of throwing. Coercion is lossy but only affects +// serialized representations; the in-memory Redux state still holds real +// `BigInt`s and every consumer reads them via the generated proto accessors. +(BigInt.prototype as unknown as { toJSON: () => string }).toJSON = function bigIntToJSON() { + return this.toString(); +}; + +export {}; diff --git a/webclient/src/services/dexie/DexieDTOs/CardDTO.ts b/webclient/src/services/dexie/DexieDTOs/CardDTO.ts index a060aedb1..dd43681ea 100644 --- a/webclient/src/services/dexie/DexieDTOs/CardDTO.ts +++ b/webclient/src/services/dexie/DexieDTOs/CardDTO.ts @@ -1,8 +1,8 @@ -import { Card } from 'types'; +import { App } from '@app/types'; import { dexieService } from '../DexieService'; -export class CardDTO extends Card { +export class CardDTO extends App.Card { save() { return dexieService.cards.put(this); } diff --git a/webclient/src/services/dexie/DexieDTOs/HostDTO.ts b/webclient/src/services/dexie/DexieDTOs/HostDTO.ts index 17f600626..e07440f64 100644 --- a/webclient/src/services/dexie/DexieDTOs/HostDTO.ts +++ b/webclient/src/services/dexie/DexieDTOs/HostDTO.ts @@ -1,14 +1,14 @@ import { IndexableType } from 'dexie'; -import { Host } from 'types'; +import { App } from '@app/types'; import { dexieService } from '../DexieService'; -export class HostDTO extends Host { +export class HostDTO extends App.Host { save() { return dexieService.hosts.put(this); } - static add(host: Host): Promise { + static add(host: App.Host): Promise { return dexieService.hosts.add(host); } @@ -20,7 +20,7 @@ export class HostDTO extends Host { return dexieService.hosts.toArray(); } - static bulkAdd(hosts: Host[]): Promise { + static bulkAdd(hosts: App.Host[]): Promise { return dexieService.hosts.bulkAdd(hosts); } diff --git a/webclient/src/services/dexie/DexieDTOs/SetDTO.ts b/webclient/src/services/dexie/DexieDTOs/SetDTO.ts index 6ae2dd20a..3bd02903c 100644 --- a/webclient/src/services/dexie/DexieDTOs/SetDTO.ts +++ b/webclient/src/services/dexie/DexieDTOs/SetDTO.ts @@ -1,8 +1,8 @@ -import { Set } from 'types'; +import { App } from '@app/types'; import { dexieService } from '../DexieService'; -export class SetDTO extends Set { +export class SetDTO extends App.Set { save() { return dexieService.sets.put(this); } diff --git a/webclient/src/services/dexie/DexieDTOs/SettingDTO.ts b/webclient/src/services/dexie/DexieDTOs/SettingDTO.ts index bbfa1680d..357ebc5a7 100644 --- a/webclient/src/services/dexie/DexieDTOs/SettingDTO.ts +++ b/webclient/src/services/dexie/DexieDTOs/SettingDTO.ts @@ -1,8 +1,8 @@ -import { Setting } from 'types'; +import { App } from '@app/types'; import { dexieService } from '../DexieService'; -export class SettingDTO extends Setting { +export class SettingDTO extends App.Setting { constructor(user) { super(); diff --git a/webclient/src/services/dexie/DexieDTOs/TokenDTO.ts b/webclient/src/services/dexie/DexieDTOs/TokenDTO.ts index 35d537709..04199e63d 100644 --- a/webclient/src/services/dexie/DexieDTOs/TokenDTO.ts +++ b/webclient/src/services/dexie/DexieDTOs/TokenDTO.ts @@ -1,8 +1,8 @@ -import { Token } from 'types'; +import { App } from '@app/types'; import { dexieService } from '../DexieService'; -export class TokenDTO extends Token { +export class TokenDTO extends App.Token { save() { return dexieService.tokens.put(this); } diff --git a/webclient/src/setupTests.ts b/webclient/src/setupTests.ts index a4fe5ea8d..d58601667 100644 --- a/webclient/src/setupTests.ts +++ b/webclient/src/setupTests.ts @@ -1,9 +1,40 @@ -// ensure jest-dom is always available during testing to cut down on boilerplate +// Install runtime polyfills (BigInt.prototype.toJSON) before any module +// under test loads — matches the production boot order in src/index.tsx. +import './polyfills'; + +// Ensure jest-dom matchers are available in every test file. import '@testing-library/jest-dom/vitest'; -// With isolate: false, all test files share the same module context. -// Restore all mocks/spies after each test to prevent leakage between tests. +// ── Global mock hygiene under `isolate: false` ──────────────────────────────── +// +// Vitest is configured with `test.isolate: false` for speed — every spec file +// in a worker shares the same module graph and the same `vi.mock` factories. +// Without aggressive per-test cleanup, state leaks trivially between tests: +// +// - A test accumulates `.mock.calls` on a shared `vi.fn()`. Later tests +// either see stale history or accidentally match on prior invocations. +// - A test installs `vi.spyOn` on a real method. Without restore, the spy +// persists into every following test and file. +// - A test swaps to fake timers. Real-time code in later tests hangs. +// +// `vi.clearAllMocks()` clears `.mock.calls` on every tracked mock without +// touching implementations — safe for module factories that produce `vi.fn()` +// instances at the top of a spec file and rely on those instances sticking +// around. `vi.restoreAllMocks()` restores original implementations on +// `vi.spyOn` targets. `vi.useRealTimers()` drops any fake-timer installation. +// +// NOTE: we intentionally do NOT call `vi.resetAllMocks()` — it resets the +// implementations of `vi.fn()` instances created inside `vi.mock(...)` +// factories, which breaks any spec that expects those mocks to persist +// across tests in the same file (e.g. `store.dispatch` mocked once at file +// load). +// +// If a specific test needs to install its own `mockReturnValue` / +// `mockImplementation`, it should set it in that test's body and rely on +// the next test overwriting or the global `clearAllMocks` clearing calls — +// it should NOT assume the mock is reset to its factory default automatically. afterEach(() => { + vi.clearAllMocks(); vi.restoreAllMocks(); vi.useRealTimers(); }); diff --git a/webclient/src/store/common/SortUtil.spec.ts b/webclient/src/store/common/SortUtil.spec.ts index acd117506..5a79b0e29 100644 --- a/webclient/src/store/common/SortUtil.spec.ts +++ b/webclient/src/store/common/SortUtil.spec.ts @@ -1,6 +1,5 @@ import { create } from '@bufbuild/protobuf'; -import { SortDirection } from 'types'; -import { ServerInfo_UserSchema } from 'generated/proto/serverinfo_user_pb'; +import { App, Data } from '@app/types'; import SortUtil from './SortUtil'; // ── sortByField ─────────────────────────────────────────────────────────────── @@ -8,48 +7,48 @@ import SortUtil from './SortUtil'; describe('sortByField', () => { it('sorts string field ASC alphabetically', () => { const arr = [{ name: 'Zane' }, { name: 'Alice' }, { name: 'Bob' }]; - SortUtil.sortByField(arr, { field: 'name', order: SortDirection.ASC }); + SortUtil.sortByField(arr, { field: 'name', order: App.SortDirection.ASC }); expect(arr.map(x => x.name)).toEqual(['Alice', 'Bob', 'Zane']); }); it('sorts string field DESC reverse-alphabetically', () => { const arr = [{ name: 'Alice' }, { name: 'Zane' }, { name: 'Bob' }]; - SortUtil.sortByField(arr, { field: 'name', order: SortDirection.DESC }); + SortUtil.sortByField(arr, { field: 'name', order: App.SortDirection.DESC }); expect(arr.map(x => x.name)).toEqual(['Zane', 'Bob', 'Alice']); }); it('sorts number field ASC', () => { const arr = [{ score: 30 }, { score: 10 }, { score: 20 }]; - SortUtil.sortByField(arr, { field: 'score', order: SortDirection.ASC }); + SortUtil.sortByField(arr, { field: 'score', order: App.SortDirection.ASC }); expect(arr.map(x => x.score)).toEqual([10, 20, 30]); }); it('sorts number field DESC', () => { const arr = [{ score: 10 }, { score: 30 }, { score: 20 }]; - SortUtil.sortByField(arr, { field: 'score', order: SortDirection.DESC }); + SortUtil.sortByField(arr, { field: 'score', order: App.SortDirection.DESC }); expect(arr.map(x => x.score)).toEqual([30, 20, 10]); }); it('no-ops on empty array without error', () => { - expect(() => SortUtil.sortByField([], { field: 'name', order: SortDirection.ASC })).not.toThrow(); + expect(() => SortUtil.sortByField([], { field: 'name', order: App.SortDirection.ASC })).not.toThrow(); }); it('sorts with nested dot-notation field', () => { const arr = [{ meta: { rank: 3 } }, { meta: { rank: 1 } }, { meta: { rank: 2 } }]; - SortUtil.sortByField(arr, { field: 'meta.rank', order: SortDirection.ASC }); + SortUtil.sortByField(arr, { field: 'meta.rank', order: App.SortDirection.ASC }); expect(arr.map(x => x.meta.rank)).toEqual([1, 2, 3]); }); it('throws when field resolves to a non-string, non-number value', () => { const arr = [{ data: {} }, { data: {} }]; - expect(() => SortUtil.sortByField(arr, { field: 'data', order: SortDirection.ASC })).toThrow( + expect(() => SortUtil.sortByField(arr, { field: 'data', order: App.SortDirection.ASC })).toThrow( 'SortField must resolve to either a string or number' ); }); it('sorts empty-string values to the bottom when sorting ASC', () => { const arr = [{ name: '' }, { name: 'Alice' }, { name: '' }]; - SortUtil.sortByField(arr, { field: 'name', order: SortDirection.ASC }); + SortUtil.sortByField(arr, { field: 'name', order: App.SortDirection.ASC }); expect(arr[0].name).toBe('Alice'); expect(arr[1].name).toBe(''); expect(arr[2].name).toBe(''); @@ -66,8 +65,8 @@ describe('sortByFields', () => { { group: 'B', name: 'Alice' }, ]; SortUtil.sortByFields(arr, [ - { field: 'group', order: SortDirection.ASC }, - { field: 'name', order: SortDirection.ASC }, + { field: 'group', order: App.SortDirection.ASC }, + { field: 'name', order: App.SortDirection.ASC }, ]); expect(arr.map(x => x.group)).toEqual(['A', 'B', 'C']); }); @@ -79,8 +78,8 @@ describe('sortByFields', () => { { group: 'B', name: 'Bob' }, ]; SortUtil.sortByFields(arr, [ - { field: 'group', order: SortDirection.ASC }, - { field: 'name', order: SortDirection.ASC }, + { field: 'group', order: App.SortDirection.ASC }, + { field: 'name', order: App.SortDirection.ASC }, ]); expect(arr[0]).toEqual({ group: 'A', name: 'Alice' }); expect(arr[1]).toEqual({ group: 'A', name: 'Zane' }); @@ -89,20 +88,20 @@ describe('sortByFields', () => { it('no-ops on empty array', () => { expect(() => - SortUtil.sortByFields([], [{ field: 'name', order: SortDirection.ASC }]) + SortUtil.sortByFields([], [{ field: 'name', order: App.SortDirection.ASC }]) ).not.toThrow(); }); it('sorts by number field', () => { const arr = [{ score: 3 }, { score: 1 }, { score: 2 }]; - SortUtil.sortByFields(arr, [{ field: 'score', order: SortDirection.ASC }]); + SortUtil.sortByFields(arr, [{ field: 'score', order: App.SortDirection.ASC }]); expect(arr.map(x => x.score)).toEqual([1, 2, 3]); }); it('returns 0 when all items tie on every sort key', () => { const arr = [{ score: 5 }, { score: 5 }]; expect(() => - SortUtil.sortByFields(arr, [{ field: 'score', order: SortDirection.ASC }]) + SortUtil.sortByFields(arr, [{ field: 'score', order: App.SortDirection.ASC }]) ).not.toThrow(); expect(arr).toHaveLength(2); }); @@ -110,7 +109,7 @@ describe('sortByFields', () => { it('throws when field resolves to a non-string, non-number value', () => { const arr = [{ data: {} }, { data: {} }]; expect(() => - SortUtil.sortByFields(arr, [{ field: 'data', order: SortDirection.ASC }]) + SortUtil.sortByFields(arr, [{ field: 'data', order: App.SortDirection.ASC }]) ).toThrow('SortField must resolve to either a string or number'); }); }); @@ -120,11 +119,11 @@ describe('sortByFields', () => { describe('sortUsersByField', () => { it('sorts by userLevel DESC first, then name ASC', () => { const users = [ - create(ServerInfo_UserSchema, { name: 'Alice', userLevel: 1, accountageSecs: 0n, privlevel: '' }), - create(ServerInfo_UserSchema, { name: 'Bob', userLevel: 8, accountageSecs: 0n, privlevel: '' }), - create(ServerInfo_UserSchema, { name: 'Carol', userLevel: 1, accountageSecs: 0n, privlevel: '' }), + create(Data.ServerInfo_UserSchema, { name: 'Alice', userLevel: 1, accountageSecs: 0n, privlevel: '' }), + create(Data.ServerInfo_UserSchema, { name: 'Bob', userLevel: 8, accountageSecs: 0n, privlevel: '' }), + create(Data.ServerInfo_UserSchema, { name: 'Carol', userLevel: 1, accountageSecs: 0n, privlevel: '' }), ]; - SortUtil.sortUsersByField(users, { field: 'name', order: SortDirection.ASC }); + SortUtil.sortUsersByField(users, { field: 'name', order: App.SortDirection.ASC }); expect(users[0].name).toBe('Bob'); expect(users[1].name).toBe('Alice'); expect(users[2].name).toBe('Carol'); @@ -132,17 +131,17 @@ describe('sortUsersByField', () => { it('no-ops on empty array', () => { expect(() => - SortUtil.sortUsersByField([], { field: 'name', order: SortDirection.ASC }) + SortUtil.sortUsersByField([], { field: 'name', order: App.SortDirection.ASC }) ).not.toThrow(); }); it('returns 0 (stable) when two users tie on both userLevel and name', () => { const users = [ - create(ServerInfo_UserSchema, { name: 'Alice', userLevel: 1, accountageSecs: 0n, privlevel: '' }), - create(ServerInfo_UserSchema, { name: 'Alice', userLevel: 1, accountageSecs: 0n, privlevel: '' }), + create(Data.ServerInfo_UserSchema, { name: 'Alice', userLevel: 1, accountageSecs: 0n, privlevel: '' }), + create(Data.ServerInfo_UserSchema, { name: 'Alice', userLevel: 1, accountageSecs: 0n, privlevel: '' }), ]; expect(() => - SortUtil.sortUsersByField(users, { field: 'name', order: SortDirection.ASC }) + SortUtil.sortUsersByField(users, { field: 'name', order: App.SortDirection.ASC }) ).not.toThrow(); expect(users).toHaveLength(2); }); @@ -152,18 +151,18 @@ describe('sortUsersByField', () => { describe('toggleSortBy', () => { it('same field + ASC → returns DESC', () => { - const result = SortUtil.toggleSortBy('name', { field: 'name', order: SortDirection.ASC }); - expect(result).toEqual({ field: 'name', order: SortDirection.DESC }); + const result = SortUtil.toggleSortBy('name', { field: 'name', order: App.SortDirection.ASC }); + expect(result).toEqual({ field: 'name', order: App.SortDirection.DESC }); }); it('same field + DESC → returns ASC', () => { - const result = SortUtil.toggleSortBy('name', { field: 'name', order: SortDirection.DESC }); - expect(result).toEqual({ field: 'name', order: SortDirection.ASC }); + const result = SortUtil.toggleSortBy('name', { field: 'name', order: App.SortDirection.DESC }); + expect(result).toEqual({ field: 'name', order: App.SortDirection.ASC }); }); it('different field → returns ASC regardless of current order', () => { - const result = SortUtil.toggleSortBy('score', { field: 'name', order: SortDirection.DESC }); - expect(result).toEqual({ field: 'score', order: SortDirection.ASC }); + const result = SortUtil.toggleSortBy('score', { field: 'name', order: App.SortDirection.DESC }); + expect(result).toEqual({ field: 'score', order: App.SortDirection.ASC }); }); }); @@ -173,7 +172,7 @@ describe('resolveFieldChain via sortByField (numeric index)', () => { it('resolves numeric index in dot-notation chain', () => { const arr = [{ items: ['c', 'b', 'a'] }, { items: ['z', 'y', 'x'] }]; // Sort by items.0 which is the first element of the items array - SortUtil.sortByField(arr, { field: 'items.0', order: SortDirection.ASC }); + SortUtil.sortByField(arr, { field: 'items.0', order: App.SortDirection.ASC }); expect(arr[0].items[0]).toBe('c'); expect(arr[1].items[0]).toBe('z'); }); diff --git a/webclient/src/store/common/SortUtil.ts b/webclient/src/store/common/SortUtil.ts index 8a15b931f..f480d1287 100644 --- a/webclient/src/store/common/SortUtil.ts +++ b/webclient/src/store/common/SortUtil.ts @@ -1,8 +1,7 @@ -import { SortBy, SortDirection } from 'types'; -import type { ServerInfo_User } from 'generated/proto/serverinfo_user_pb'; +import { App, Data } from '@app/types'; export default class SortUtil { - static sortByField(arr: T[], sortBy: SortBy): void { + static sortByField(arr: T[], sortBy: App.SortBy): void { if (arr.length) { const field = SortUtil.resolveFieldChain(arr[0], sortBy.field); const fieldType = typeof field; @@ -21,7 +20,7 @@ export default class SortUtil { } } - static sortByFields(arr: T[], sorts: SortBy[]) { + static sortByFields(arr: T[], sorts: App.SortBy[]) { if (arr.length) { arr.sort((a, b) => { for (let i = 0; i < sorts.length; i++) { @@ -52,35 +51,35 @@ export default class SortUtil { } } - static sortUsersByField(users: ServerInfo_User[], sortBy: SortBy) { + static sortUsersByField(users: Data.ServerInfo_User[], sortBy: App.SortBy) { if (users.length) { users.sort((a, b) => SortUtil.userComparator(a, b, sortBy)) } } - static toggleSortBy(field: F, sortBy: SortBy): { field: F; order: SortDirection } { + static toggleSortBy(field: F, sortBy: App.SortBy): { field: F; order: App.SortDirection } { const sameField = field === sortBy.field; - const isASC = sortBy.order === SortDirection.ASC; + const isASC = sortBy.order === App.SortDirection.ASC; return { field, - order: sameField && isASC ? SortDirection.DESC : SortDirection.ASC + order: sameField && isASC ? App.SortDirection.DESC : App.SortDirection.ASC } } - private static sortByNumber(arr: T[], sortBy: SortBy): void { + private static sortByNumber(arr: T[], sortBy: App.SortBy): void { arr.sort((a, b) => SortUtil.numberComparator(a, b, sortBy)); } - private static sortByString(arr: T[], sortBy: SortBy): void { + private static sortByString(arr: T[], sortBy: App.SortBy): void { arr.sort((a, b) => SortUtil.stringComparator(a, b, sortBy)); } - private static userComparator(a: ServerInfo_User, b: ServerInfo_User, sortBy: SortBy, sortByUserLevel = true) { + private static userComparator(a: Data.ServerInfo_User, b: Data.ServerInfo_User, sortBy: App.SortBy, sortByUserLevel = true) { if (sortByUserLevel) { const adminSortBy = { field: 'userLevel', - order: SortDirection.DESC + order: App.SortDirection.DESC }; const adminSorted = SortUtil.numberComparator(a, b, adminSortBy); @@ -99,18 +98,18 @@ export default class SortUtil { return 0; } - private static numberComparator(a: T, b: T, { field, order }: SortBy) { + private static numberComparator(a: T, b: T, { field, order }: App.SortBy) { const aResolved = SortUtil.resolveFieldChain(a, field); const bResolved = SortUtil.resolveFieldChain(b, field); - if (order === SortDirection.ASC) { + if (order === App.SortDirection.ASC) { return aResolved - bResolved; } else { return bResolved - aResolved; } } - private static stringComparator(a: T, b: T, { field, order }: SortBy) { + private static stringComparator(a: T, b: T, { field, order }: App.SortBy) { const aResolved = SortUtil.resolveFieldChain(a, field); const bResolved = SortUtil.resolveFieldChain(b, field); @@ -125,7 +124,7 @@ export default class SortUtil { return -1; } - if (order === SortDirection.ASC) { + if (order === App.SortDirection.ASC) { return aResolved.localeCompare(bResolved); } else { return bResolved.localeCompare(aResolved); diff --git a/webclient/src/store/common/normalizers.spec.ts b/webclient/src/store/common/normalizers.spec.ts index ddd0a8f1e..f157b93dd 100644 --- a/webclient/src/store/common/normalizers.spec.ts +++ b/webclient/src/store/common/normalizers.spec.ts @@ -1,18 +1,15 @@ import { normalizeRoomInfo, normalizeGameObject, normalizeLogs, normalizeBannedUserError, normalizeUserMessage } from './normalizers'; import { create } from '@bufbuild/protobuf'; -import { ServerInfo_RoomSchema } from 'generated/proto/serverinfo_room_pb'; -import { ServerInfo_GameSchema } from 'generated/proto/serverinfo_game_pb'; -import { Event_RoomSaySchema } from 'generated/proto/event_room_say_pb'; -import { Message } from 'types'; +import { Data, Enriched } from '@app/types'; describe('normalizeRoomInfo', () => { it('builds gametypeMap from gametypeList and normalises games', () => { - const room = create(ServerInfo_RoomSchema, { + const room = create(Data.ServerInfo_RoomSchema, { roomId: 1, name: 'Lobby', gametypeList: [{ gameTypeId: 1, description: 'Standard' }], gameList: [ - create(ServerInfo_GameSchema, { gameId: 10, gameTypes: [1], description: 'My Game' }), + create(Data.ServerInfo_GameSchema, { gameId: 10, gameTypes: [1], description: 'My Game' }), ], }); @@ -25,7 +22,7 @@ describe('normalizeRoomInfo', () => { }); it('handles room with empty gametypeList', () => { - const room = create(ServerInfo_RoomSchema, { roomId: 2, name: 'Empty' }); + const room = create(Data.ServerInfo_RoomSchema, { roomId: 2, name: 'Empty' }); const result = normalizeRoomInfo(room); expect(result.gametypeMap).toEqual({}); expect(result.gameList).toEqual([]); @@ -34,19 +31,19 @@ describe('normalizeRoomInfo', () => { describe('normalizeGameObject', () => { it('maps gameTypes[0] to gameType string via gametypeMap', () => { - const game = create(ServerInfo_GameSchema, { gameId: 1, gameTypes: [5] }); + const game = create(Data.ServerInfo_GameSchema, { gameId: 1, gameTypes: [5] }); const result = normalizeGameObject(game, { 5: 'Legacy' }); expect(result.gameType).toBe('Legacy'); }); it('returns empty string when no gameTypes', () => { - const game = create(ServerInfo_GameSchema, { gameId: 2 }); + const game = create(Data.ServerInfo_GameSchema, { gameId: 2 }); const result = normalizeGameObject(game, {}); expect(result.gameType).toBe(''); }); it('fills empty description with empty string', () => { - const game = create(ServerInfo_GameSchema, { gameId: 3 }); + const game = create(Data.ServerInfo_GameSchema, { gameId: 3 }); const result = normalizeGameObject(game, {}); expect(result.description).toBe(''); }); @@ -55,10 +52,10 @@ describe('normalizeGameObject', () => { describe('normalizeLogs', () => { it('groups logs by targetType', () => { const logs = [ - { targetType: 'room' }, - { targetType: 'game' }, - { targetType: 'room' }, - ] as any[]; + create(Data.ServerInfo_ChatMessageSchema, { targetType: 'room' }), + create(Data.ServerInfo_ChatMessageSchema, { targetType: 'game' }), + create(Data.ServerInfo_ChatMessageSchema, { targetType: 'room' }), + ]; const result = normalizeLogs(logs); expect(result.room).toHaveLength(2); expect(result.game).toHaveLength(1); @@ -71,7 +68,7 @@ describe('normalizeLogs', () => { }); describe('normalizeBannedUserError', () => { - it('returns permanently banned message when endTime is 0', () => { + it('returns permanently banned Enriched.Message when endTime is 0', () => { expect(normalizeBannedUserError('', 0)).toBe('You are permanently banned'); }); @@ -92,11 +89,11 @@ describe('normalizeBannedUserError', () => { }); describe('normalizeUserMessage', () => { - const makeMsg = (fields: Partial): Message => ({ - ...create(Event_RoomSaySchema), + const makeMsg = (fields: Partial): Enriched.Message => ({ + ...create(Data.Event_RoomSaySchema), timeReceived: 0, ...fields, - } as Message); + } as Enriched.Message); it('prepends "name: " to message when name is present', () => { const result = normalizeUserMessage(makeMsg({ name: 'Alice', message: 'hello' })); diff --git a/webclient/src/store/common/normalizers.ts b/webclient/src/store/common/normalizers.ts index a68949984..ac3af7ce4 100644 --- a/webclient/src/store/common/normalizers.ts +++ b/webclient/src/store/common/normalizers.ts @@ -1,19 +1,15 @@ -import type { ServerInfo_Room } from 'generated/proto/serverinfo_room_pb'; -import type { ServerInfo_Game } from 'generated/proto/serverinfo_game_pb'; -import type { ServerInfo_GameType } from 'generated/proto/serverinfo_gametype_pb'; -import type { ServerInfo_ChatMessage } from 'generated/proto/serverinfo_chat_message_pb'; -import { Game, GametypeMap, LogGroups, Message, Room } from 'types'; +import { Data, Enriched } from '@app/types'; /** Flatten a gametype list into a lookup map of { gameTypeId → description }. */ -export function normalizeGametypeMap(gametypeList: ServerInfo_GameType[]): GametypeMap { - return gametypeList.reduce((map, type) => { +export function normalizeGametypeMap(gametypeList: Data.ServerInfo_GameType[]): Enriched.GametypeMap { + return gametypeList.reduce((map, type) => { map[type.gameTypeId] = type.description; return map; }, {}); } /** Flatten room gameTypes into a map object and normalize all games inside. */ -export function normalizeRoomInfo(roomInfo: ServerInfo_Room): Room { +export function normalizeRoomInfo(roomInfo: Data.ServerInfo_Room): Enriched.Room { const gametypeMap = normalizeGametypeMap(roomInfo.gametypeList); const gameList = roomInfo.gameList.map( @@ -29,7 +25,7 @@ export function normalizeRoomInfo(roomInfo: ServerInfo_Room): Room { } /** Flatten gameTypes[] into a gameType string; fill in default sortable values. */ -export function normalizeGameObject(game: ServerInfo_Game, gametypeMap: GametypeMap): Game { +export function normalizeGameObject(game: Data.ServerInfo_Game, gametypeMap: Enriched.GametypeMap): Enriched.Game { const { gameTypes, description } = game; const hasType = gameTypes && gameTypes.length; @@ -41,13 +37,13 @@ export function normalizeGameObject(game: ServerInfo_Game, gametypeMap: Gametype } /** Group a flat LogItem[] into { room, game, chat } buckets for the server store. */ -export function normalizeLogs(logs: ServerInfo_ChatMessage[]): LogGroups { +export function normalizeLogs(logs: Data.ServerInfo_ChatMessage[]): Enriched.LogGroups { return logs.reduce((obj, log) => { - const type = log.targetType as keyof LogGroups; + const type = log.targetType as keyof Enriched.LogGroups; obj[type] = obj[type] || []; obj[type]!.push(log); return obj; - }, {} as LogGroups); + }, {} as Enriched.LogGroups); } /** @@ -56,7 +52,7 @@ export function normalizeLogs(logs: ServerInfo_ChatMessage[]): LogGroups { * so this is a no-op for those. * Returns a new Message — does not mutate the original. */ -export function normalizeUserMessage(message: Message): Message { +export function normalizeUserMessage(message: Enriched.Message): Enriched.Message { if (!message.name) { return message; } diff --git a/webclient/src/store/game/__mocks__/fixtures.ts b/webclient/src/store/game/__mocks__/fixtures.ts index 87a9ea346..cd5d1f15d 100644 --- a/webclient/src/store/game/__mocks__/fixtures.ts +++ b/webclient/src/store/game/__mocks__/fixtures.ts @@ -1,18 +1,10 @@ -import { ProtoInit } from 'types'; -import type { ServerInfo_Card } from 'generated/proto/serverinfo_card_pb'; -import type { ServerInfo_Counter } from 'generated/proto/serverinfo_counter_pb'; -import type { ServerInfo_Arrow } from 'generated/proto/serverinfo_arrow_pb'; -import type { ServerInfo_PlayerProperties } from 'generated/proto/serverinfo_playerproperties_pb'; +import type { MessageInitShape } from '@bufbuild/protobuf'; +import { Data } from '@app/types'; import { create } from '@bufbuild/protobuf'; -import { ServerInfo_CardSchema } from 'generated/proto/serverinfo_card_pb'; -import { ServerInfo_CounterSchema } from 'generated/proto/serverinfo_counter_pb'; -import { colorSchema } from 'generated/proto/color_pb'; -import { ServerInfo_ArrowSchema } from 'generated/proto/serverinfo_arrow_pb'; -import { ServerInfo_PlayerPropertiesSchema } from 'generated/proto/serverinfo_playerproperties_pb'; import { GameEntry, GamesState, PlayerEntry, ZoneEntry } from '../game.interfaces'; -export function makeCard(overrides: ProtoInit = {}): ServerInfo_Card { - return create(ServerInfo_CardSchema, { +export function makeCard(overrides: MessageInitShape = {}): Data.ServerInfo_Card { + return create(Data.ServerInfo_CardSchema, { id: 1, name: 'Test Card', x: 0, @@ -34,19 +26,19 @@ export function makeCard(overrides: ProtoInit = {}): ServerInfo }); } -export function makeCounter(overrides: ProtoInit = {}): ServerInfo_Counter { - return create(ServerInfo_CounterSchema, { +export function makeCounter(overrides: MessageInitShape = {}): Data.ServerInfo_Counter { + return create(Data.ServerInfo_CounterSchema, { id: 1, name: 'Life', - counterColor: create(colorSchema, { r: 0, g: 0, b: 0, a: 255 }), + counterColor: create(Data.colorSchema, { r: 0, g: 0, b: 0, a: 255 }), radius: 1, count: 20, ...overrides, }); } -export function makeArrow(overrides: ProtoInit = {}): ServerInfo_Arrow { - return create(ServerInfo_ArrowSchema, { +export function makeArrow(overrides: MessageInitShape = {}): Data.ServerInfo_Arrow { + return create(Data.ServerInfo_ArrowSchema, { id: 1, startPlayerId: 1, startZone: 'table', @@ -54,7 +46,7 @@ export function makeArrow(overrides: ProtoInit = {}): ServerIn targetPlayerId: 1, targetZone: 'table', targetCardId: 2, - arrowColor: create(colorSchema, { r: 255, g: 0, b: 0, a: 255 }), + arrowColor: create(Data.colorSchema, { r: 255, g: 0, b: 0, a: 255 }), ...overrides, }); } @@ -72,8 +64,10 @@ export function makeZoneEntry(overrides: Partial = {}): ZoneEntry { }; } -export function makePlayerProperties(overrides: ProtoInit = {}): ServerInfo_PlayerProperties { - return create(ServerInfo_PlayerPropertiesSchema, { +export function makePlayerProperties( + overrides: MessageInitShape = {}, +): Data.ServerInfo_PlayerProperties { + return create(Data.ServerInfo_PlayerPropertiesSchema, { playerId: 1, spectator: false, conceded: false, diff --git a/webclient/src/store/game/game.actions.spec.ts b/webclient/src/store/game/game.actions.spec.ts index 00afd55d9..b75282943 100644 --- a/webclient/src/store/game/game.actions.spec.ts +++ b/webclient/src/store/game/game.actions.spec.ts @@ -1,32 +1,13 @@ import { create } from '@bufbuild/protobuf'; +import { Data } from '@app/types'; import { Actions } from './game.actions'; import { Types } from './game.types'; import { makeArrow, makeCard, makeCounter, - makeGameEntry, makePlayerProperties, } from './__mocks__/fixtures'; -import { Event_GameStateChangedSchema } from 'generated/proto/event_game_state_changed_pb'; -import { Event_MoveCardSchema } from 'generated/proto/event_move_card_pb'; -import { Event_FlipCardSchema } from 'generated/proto/event_flip_card_pb'; -import { Event_DestroyCardSchema } from 'generated/proto/event_destroy_card_pb'; -import { Event_AttachCardSchema } from 'generated/proto/event_attach_card_pb'; -import { Event_CreateTokenSchema } from 'generated/proto/event_create_token_pb'; -import { Event_SetCardAttrSchema } from 'generated/proto/event_set_card_attr_pb'; -import { Event_SetCardCounterSchema } from 'generated/proto/event_set_card_counter_pb'; -import { Event_CreateArrowSchema } from 'generated/proto/event_create_arrow_pb'; -import { Event_DeleteArrowSchema } from 'generated/proto/event_delete_arrow_pb'; -import { Event_CreateCounterSchema } from 'generated/proto/event_create_counter_pb'; -import { Event_SetCounterSchema } from 'generated/proto/event_set_counter_pb'; -import { Event_DelCounterSchema } from 'generated/proto/event_del_counter_pb'; -import { Event_DrawCardsSchema } from 'generated/proto/event_draw_cards_pb'; -import { Event_RevealCardsSchema } from 'generated/proto/event_reveal_cards_pb'; -import { Event_ShuffleSchema } from 'generated/proto/event_shuffle_pb'; -import { Event_RollDieSchema } from 'generated/proto/event_roll_die_pb'; -import { Event_DumpZoneSchema } from 'generated/proto/event_dump_zone_pb'; -import { Event_ChangeZonePropertiesSchema } from 'generated/proto/event_change_zone_properties_pb'; describe('Actions', () => { it('clearStore', () => { @@ -34,8 +15,8 @@ describe('Actions', () => { }); it('gameJoined', () => { - const entry = makeGameEntry(); - expect(Actions.gameJoined(1, entry)).toEqual({ type: Types.GAME_JOINED, gameId: 1, gameEntry: entry }); + const data = create(Data.Event_GameJoinedSchema, { hostId: 1, playerId: 2 }); + expect(Actions.gameJoined(data)).toEqual({ type: Types.GAME_JOINED, data }); }); it('gameLeft', () => { @@ -51,7 +32,7 @@ describe('Actions', () => { }); it('gameStateChanged', () => { - const data = create(Event_GameStateChangedSchema, { + const data = create(Data.Event_GameStateChangedSchema, { playerList: [], gameStarted: true, activePlayerId: 1, activePhase: 0, secondsElapsed: 0 }); expect(Actions.gameStateChanged(1, data)).toEqual({ type: Types.GAME_STATE_CHANGED, gameId: 1, data }); @@ -81,85 +62,85 @@ describe('Actions', () => { }); it('cardMoved', () => { - const data = create(Event_MoveCardSchema, { cardId: 1 }); + const data = create(Data.Event_MoveCardSchema, { cardId: 1 }); expect(Actions.cardMoved(1, 2, data)).toEqual({ type: Types.CARD_MOVED, gameId: 1, playerId: 2, data }); }); it('cardFlipped', () => { - const data = create(Event_FlipCardSchema, { cardId: 1 }); + const data = create(Data.Event_FlipCardSchema, { cardId: 1 }); expect(Actions.cardFlipped(1, 2, data)).toEqual({ type: Types.CARD_FLIPPED, gameId: 1, playerId: 2, data }); }); it('cardDestroyed', () => { - const data = create(Event_DestroyCardSchema, { cardId: 1 }); + const data = create(Data.Event_DestroyCardSchema, { cardId: 1 }); expect(Actions.cardDestroyed(1, 2, data)).toEqual({ type: Types.CARD_DESTROYED, gameId: 1, playerId: 2, data }); }); it('cardAttached', () => { - const data = create(Event_AttachCardSchema, { cardId: 1 }); + const data = create(Data.Event_AttachCardSchema, { cardId: 1 }); expect(Actions.cardAttached(1, 2, data)).toEqual({ type: Types.CARD_ATTACHED, gameId: 1, playerId: 2, data }); }); it('tokenCreated', () => { - const data = create(Event_CreateTokenSchema, { cardId: 1 }); + const data = create(Data.Event_CreateTokenSchema, { cardId: 1 }); expect(Actions.tokenCreated(1, 2, data)).toEqual({ type: Types.TOKEN_CREATED, gameId: 1, playerId: 2, data }); }); it('cardAttrChanged', () => { - const data = create(Event_SetCardAttrSchema, { cardId: 1 }); + const data = create(Data.Event_SetCardAttrSchema, { cardId: 1 }); expect(Actions.cardAttrChanged(1, 2, data)).toEqual({ type: Types.CARD_ATTR_CHANGED, gameId: 1, playerId: 2, data }); }); it('cardCounterChanged', () => { - const data = create(Event_SetCardCounterSchema, { cardId: 1 }); + const data = create(Data.Event_SetCardCounterSchema, { cardId: 1 }); expect(Actions.cardCounterChanged(1, 2, data)).toEqual({ type: Types.CARD_COUNTER_CHANGED, gameId: 1, playerId: 2, data }); }); it('arrowCreated', () => { const arrow = makeArrow(); - const data = create(Event_CreateArrowSchema, { arrowInfo: arrow }); + const data = create(Data.Event_CreateArrowSchema, { arrowInfo: arrow }); expect(Actions.arrowCreated(1, 2, data)).toEqual({ type: Types.ARROW_CREATED, gameId: 1, playerId: 2, data }); }); it('arrowDeleted', () => { - const data = create(Event_DeleteArrowSchema, { arrowId: 3 }); + const data = create(Data.Event_DeleteArrowSchema, { arrowId: 3 }); expect(Actions.arrowDeleted(1, 2, data)).toEqual({ type: Types.ARROW_DELETED, gameId: 1, playerId: 2, data }); }); it('counterCreated', () => { const counter = makeCounter(); - const data = create(Event_CreateCounterSchema, { counterInfo: counter }); + const data = create(Data.Event_CreateCounterSchema, { counterInfo: counter }); expect(Actions.counterCreated(1, 2, data)).toEqual({ type: Types.COUNTER_CREATED, gameId: 1, playerId: 2, data }); }); it('counterSet', () => { - const data = create(Event_SetCounterSchema, { counterId: 1, value: 10 }); + const data = create(Data.Event_SetCounterSchema, { counterId: 1, value: 10 }); expect(Actions.counterSet(1, 2, data)).toEqual({ type: Types.COUNTER_SET, gameId: 1, playerId: 2, data }); }); it('counterDeleted', () => { - const data = create(Event_DelCounterSchema, { counterId: 1 }); + const data = create(Data.Event_DelCounterSchema, { counterId: 1 }); expect(Actions.counterDeleted(1, 2, data)).toEqual({ type: Types.COUNTER_DELETED, gameId: 1, playerId: 2, data }); }); it('cardsDrawn', () => { const card = makeCard(); - const data = create(Event_DrawCardsSchema, { number: 2, cards: [card] }); + const data = create(Data.Event_DrawCardsSchema, { number: 2, cards: [card] }); expect(Actions.cardsDrawn(1, 2, data)).toEqual({ type: Types.CARDS_DRAWN, gameId: 1, playerId: 2, data }); }); it('cardsRevealed', () => { - const data = create(Event_RevealCardsSchema, { zoneName: 'hand', cards: [] }); + const data = create(Data.Event_RevealCardsSchema, { zoneName: 'hand', cards: [] }); expect(Actions.cardsRevealed(1, 2, data)).toEqual({ type: Types.CARDS_REVEALED, gameId: 1, playerId: 2, data }); }); it('zoneShuffled', () => { - const data = create(Event_ShuffleSchema, { zoneName: 'deck', start: 0, end: 39 }); + const data = create(Data.Event_ShuffleSchema, { zoneName: 'deck', start: 0, end: 39 }); expect(Actions.zoneShuffled(1, 2, data)).toEqual({ type: Types.ZONE_SHUFFLED, gameId: 1, playerId: 2, data }); }); it('dieRolled', () => { - const data = create(Event_RollDieSchema, { sides: 6, value: 4, values: [4] }); + const data = create(Data.Event_RollDieSchema, { sides: 6, value: 4, values: [4] }); expect(Actions.dieRolled(1, 2, data)).toEqual({ type: Types.DIE_ROLLED, gameId: 1, playerId: 2, data }); }); @@ -176,12 +157,12 @@ describe('Actions', () => { }); it('zoneDumped', () => { - const data = create(Event_DumpZoneSchema, { zoneOwnerId: 1, zoneName: 'hand', numberCards: 3, isReversed: false }); + const data = create(Data.Event_DumpZoneSchema, { zoneOwnerId: 1, zoneName: 'hand', numberCards: 3, isReversed: false }); expect(Actions.zoneDumped(1, 2, data)).toEqual({ type: Types.ZONE_DUMPED, gameId: 1, playerId: 2, data }); }); it('zonePropertiesChanged', () => { - const data = create(Event_ChangeZonePropertiesSchema, { zoneName: 'deck', alwaysRevealTopCard: true, alwaysLookAtTopCard: false }); + const data = create(Data.Event_ChangeZonePropertiesSchema, { zoneName: 'deck', alwaysRevealTopCard: true, alwaysLookAtTopCard: false }); expect(Actions.zonePropertiesChanged(1, 2, data)).toEqual({ type: Types.ZONE_PROPERTIES_CHANGED, gameId: 1, diff --git a/webclient/src/store/game/game.actions.ts b/webclient/src/store/game/game.actions.ts index d02b5d402..91a2be55a 100644 --- a/webclient/src/store/game/game.actions.ts +++ b/webclient/src/store/game/game.actions.ts @@ -1,24 +1,4 @@ -import type { Event_AttachCard } from 'generated/proto/event_attach_card_pb'; -import type { Event_ChangeZoneProperties } from 'generated/proto/event_change_zone_properties_pb'; -import type { Event_CreateArrow } from 'generated/proto/event_create_arrow_pb'; -import type { Event_CreateCounter } from 'generated/proto/event_create_counter_pb'; -import type { Event_CreateToken } from 'generated/proto/event_create_token_pb'; -import type { Event_DelCounter } from 'generated/proto/event_del_counter_pb'; -import type { Event_DeleteArrow } from 'generated/proto/event_delete_arrow_pb'; -import type { Event_DestroyCard } from 'generated/proto/event_destroy_card_pb'; -import type { Event_DrawCards } from 'generated/proto/event_draw_cards_pb'; -import type { Event_DumpZone } from 'generated/proto/event_dump_zone_pb'; -import type { Event_FlipCard } from 'generated/proto/event_flip_card_pb'; -import type { Event_GameStateChanged } from 'generated/proto/event_game_state_changed_pb'; -import type { Event_MoveCard } from 'generated/proto/event_move_card_pb'; -import type { ServerInfo_PlayerProperties } from 'generated/proto/serverinfo_playerproperties_pb'; -import type { Event_RevealCards } from 'generated/proto/event_reveal_cards_pb'; -import type { Event_RollDie } from 'generated/proto/event_roll_die_pb'; -import type { Event_SetCardAttr } from 'generated/proto/event_set_card_attr_pb'; -import type { Event_SetCardCounter } from 'generated/proto/event_set_card_counter_pb'; -import type { Event_SetCounter } from 'generated/proto/event_set_counter_pb'; -import type { Event_Shuffle } from 'generated/proto/event_shuffle_pb'; -import { GameEntry } from './game.interfaces'; +import type { Data } from '@app/types'; import { Types } from './game.types'; export const Actions = { @@ -26,10 +6,9 @@ export const Actions = { type: Types.CLEAR_STORE, }), - gameJoined: (gameId: number, gameEntry: GameEntry) => ({ + gameJoined: (data: Data.Event_GameJoined) => ({ type: Types.GAME_JOINED, - gameId, - gameEntry, + data, }), gameLeft: (gameId: number) => ({ @@ -48,13 +27,13 @@ export const Actions = { hostId, }), - gameStateChanged: (gameId: number, data: Event_GameStateChanged) => ({ + gameStateChanged: (gameId: number, data: Data.Event_GameStateChanged) => ({ type: Types.GAME_STATE_CHANGED, gameId, data, }), - playerJoined: (gameId: number, playerProperties: ServerInfo_PlayerProperties) => ({ + playerJoined: (gameId: number, playerProperties: Data.ServerInfo_PlayerProperties) => ({ type: Types.PLAYER_JOINED, gameId, playerProperties, @@ -67,7 +46,7 @@ export const Actions = { reason, }), - playerPropertiesChanged: (gameId: number, playerId: number, properties: ServerInfo_PlayerProperties) => ({ + playerPropertiesChanged: (gameId: number, playerId: number, properties: Data.ServerInfo_PlayerProperties) => ({ type: Types.PLAYER_PROPERTIES_CHANGED, gameId, playerId, @@ -79,112 +58,112 @@ export const Actions = { gameId, }), - cardMoved: (gameId: number, playerId: number, data: Event_MoveCard) => ({ + cardMoved: (gameId: number, playerId: number, data: Data.Event_MoveCard) => ({ type: Types.CARD_MOVED, gameId, playerId, data, }), - cardFlipped: (gameId: number, playerId: number, data: Event_FlipCard) => ({ + cardFlipped: (gameId: number, playerId: number, data: Data.Event_FlipCard) => ({ type: Types.CARD_FLIPPED, gameId, playerId, data, }), - cardDestroyed: (gameId: number, playerId: number, data: Event_DestroyCard) => ({ + cardDestroyed: (gameId: number, playerId: number, data: Data.Event_DestroyCard) => ({ type: Types.CARD_DESTROYED, gameId, playerId, data, }), - cardAttached: (gameId: number, playerId: number, data: Event_AttachCard) => ({ + cardAttached: (gameId: number, playerId: number, data: Data.Event_AttachCard) => ({ type: Types.CARD_ATTACHED, gameId, playerId, data, }), - tokenCreated: (gameId: number, playerId: number, data: Event_CreateToken) => ({ + tokenCreated: (gameId: number, playerId: number, data: Data.Event_CreateToken) => ({ type: Types.TOKEN_CREATED, gameId, playerId, data, }), - cardAttrChanged: (gameId: number, playerId: number, data: Event_SetCardAttr) => ({ + cardAttrChanged: (gameId: number, playerId: number, data: Data.Event_SetCardAttr) => ({ type: Types.CARD_ATTR_CHANGED, gameId, playerId, data, }), - cardCounterChanged: (gameId: number, playerId: number, data: Event_SetCardCounter) => ({ + cardCounterChanged: (gameId: number, playerId: number, data: Data.Event_SetCardCounter) => ({ type: Types.CARD_COUNTER_CHANGED, gameId, playerId, data, }), - arrowCreated: (gameId: number, playerId: number, data: Event_CreateArrow) => ({ + arrowCreated: (gameId: number, playerId: number, data: Data.Event_CreateArrow) => ({ type: Types.ARROW_CREATED, gameId, playerId, data, }), - arrowDeleted: (gameId: number, playerId: number, data: Event_DeleteArrow) => ({ + arrowDeleted: (gameId: number, playerId: number, data: Data.Event_DeleteArrow) => ({ type: Types.ARROW_DELETED, gameId, playerId, data, }), - counterCreated: (gameId: number, playerId: number, data: Event_CreateCounter) => ({ + counterCreated: (gameId: number, playerId: number, data: Data.Event_CreateCounter) => ({ type: Types.COUNTER_CREATED, gameId, playerId, data, }), - counterSet: (gameId: number, playerId: number, data: Event_SetCounter) => ({ + counterSet: (gameId: number, playerId: number, data: Data.Event_SetCounter) => ({ type: Types.COUNTER_SET, gameId, playerId, data, }), - counterDeleted: (gameId: number, playerId: number, data: Event_DelCounter) => ({ + counterDeleted: (gameId: number, playerId: number, data: Data.Event_DelCounter) => ({ type: Types.COUNTER_DELETED, gameId, playerId, data, }), - cardsDrawn: (gameId: number, playerId: number, data: Event_DrawCards) => ({ + cardsDrawn: (gameId: number, playerId: number, data: Data.Event_DrawCards) => ({ type: Types.CARDS_DRAWN, gameId, playerId, data, }), - cardsRevealed: (gameId: number, playerId: number, data: Event_RevealCards) => ({ + cardsRevealed: (gameId: number, playerId: number, data: Data.Event_RevealCards) => ({ type: Types.CARDS_REVEALED, gameId, playerId, data, }), - zoneShuffled: (gameId: number, playerId: number, data: Event_Shuffle) => ({ + zoneShuffled: (gameId: number, playerId: number, data: Data.Event_Shuffle) => ({ type: Types.ZONE_SHUFFLED, gameId, playerId, data, }), - dieRolled: (gameId: number, playerId: number, data: Event_RollDie) => ({ + dieRolled: (gameId: number, playerId: number, data: Data.Event_RollDie) => ({ type: Types.DIE_ROLLED, gameId, playerId, @@ -209,14 +188,14 @@ export const Actions = { reversed, }), - zoneDumped: (gameId: number, playerId: number, data: Event_DumpZone) => ({ + zoneDumped: (gameId: number, playerId: number, data: Data.Event_DumpZone) => ({ type: Types.ZONE_DUMPED, gameId, playerId, data, }), - zonePropertiesChanged: (gameId: number, playerId: number, data: Event_ChangeZoneProperties) => ({ + zonePropertiesChanged: (gameId: number, playerId: number, data: Data.Event_ChangeZoneProperties) => ({ type: Types.ZONE_PROPERTIES_CHANGED, gameId, playerId, diff --git a/webclient/src/store/game/game.dispatch.spec.ts b/webclient/src/store/game/game.dispatch.spec.ts index 77c6e0227..a06d940e4 100644 --- a/webclient/src/store/game/game.dispatch.spec.ts +++ b/webclient/src/store/game/game.dispatch.spec.ts @@ -1,37 +1,16 @@ -vi.mock('store/store', () => ({ store: { dispatch: vi.fn() } })); +vi.mock('../store', () => ({ store: { dispatch: vi.fn() } })); import { create } from '@bufbuild/protobuf'; -import { store } from 'store/store'; +import { Data } from '@app/types'; +import { store } from '..'; import { Actions } from './game.actions'; import { Dispatch } from './game.dispatch'; import { makeArrow, makeCard, makeCounter, - makeGameEntry, makePlayerProperties, } from './__mocks__/fixtures'; -import { Event_GameStateChangedSchema } from 'generated/proto/event_game_state_changed_pb'; -import { Event_MoveCardSchema } from 'generated/proto/event_move_card_pb'; -import { Event_FlipCardSchema } from 'generated/proto/event_flip_card_pb'; -import { Event_DestroyCardSchema } from 'generated/proto/event_destroy_card_pb'; -import { Event_AttachCardSchema } from 'generated/proto/event_attach_card_pb'; -import { Event_CreateTokenSchema } from 'generated/proto/event_create_token_pb'; -import { Event_SetCardAttrSchema } from 'generated/proto/event_set_card_attr_pb'; -import { Event_SetCardCounterSchema } from 'generated/proto/event_set_card_counter_pb'; -import { Event_CreateArrowSchema } from 'generated/proto/event_create_arrow_pb'; -import { Event_DeleteArrowSchema } from 'generated/proto/event_delete_arrow_pb'; -import { Event_CreateCounterSchema } from 'generated/proto/event_create_counter_pb'; -import { Event_SetCounterSchema } from 'generated/proto/event_set_counter_pb'; -import { Event_DelCounterSchema } from 'generated/proto/event_del_counter_pb'; -import { Event_DrawCardsSchema } from 'generated/proto/event_draw_cards_pb'; -import { Event_RevealCardsSchema } from 'generated/proto/event_reveal_cards_pb'; -import { Event_ShuffleSchema } from 'generated/proto/event_shuffle_pb'; -import { Event_RollDieSchema } from 'generated/proto/event_roll_die_pb'; -import { Event_DumpZoneSchema } from 'generated/proto/event_dump_zone_pb'; -import { Event_ChangeZonePropertiesSchema } from 'generated/proto/event_change_zone_properties_pb'; - -beforeEach(() => vi.clearAllMocks()); describe('Dispatch', () => { it('clearStore dispatches Actions.clearStore()', () => { @@ -40,9 +19,9 @@ describe('Dispatch', () => { }); it('gameJoined dispatches Actions.gameJoined()', () => { - const entry = makeGameEntry(); - Dispatch.gameJoined(1, entry); - expect(store.dispatch).toHaveBeenCalledWith(Actions.gameJoined(1, entry)); + const data = create(Data.Event_GameJoinedSchema, { hostId: 1, playerId: 2 }); + Dispatch.gameJoined(data); + expect(store.dispatch).toHaveBeenCalledWith(Actions.gameJoined(data)); }); it('gameLeft dispatches Actions.gameLeft()', () => { @@ -61,7 +40,7 @@ describe('Dispatch', () => { }); it('gameStateChanged dispatches Actions.gameStateChanged()', () => { - const data = create(Event_GameStateChangedSchema, { + const data = create(Data.Event_GameStateChangedSchema, { playerList: [], gameStarted: false, activePlayerId: 0, activePhase: 0, secondsElapsed: 0 }); Dispatch.gameStateChanged(1, data); @@ -91,97 +70,97 @@ describe('Dispatch', () => { }); it('cardMoved dispatches Actions.cardMoved()', () => { - const data = create(Event_MoveCardSchema, { cardId: 1 }); + const data = create(Data.Event_MoveCardSchema, { cardId: 1 }); Dispatch.cardMoved(1, 2, data); expect(store.dispatch).toHaveBeenCalledWith(Actions.cardMoved(1, 2, data)); }); it('cardFlipped dispatches Actions.cardFlipped()', () => { - const data = create(Event_FlipCardSchema, { cardId: 1 }); + const data = create(Data.Event_FlipCardSchema, { cardId: 1 }); Dispatch.cardFlipped(1, 2, data); expect(store.dispatch).toHaveBeenCalledWith(Actions.cardFlipped(1, 2, data)); }); it('cardDestroyed dispatches Actions.cardDestroyed()', () => { - const data = create(Event_DestroyCardSchema, { cardId: 1 }); + const data = create(Data.Event_DestroyCardSchema, { cardId: 1 }); Dispatch.cardDestroyed(1, 2, data); expect(store.dispatch).toHaveBeenCalledWith(Actions.cardDestroyed(1, 2, data)); }); it('cardAttached dispatches Actions.cardAttached()', () => { - const data = create(Event_AttachCardSchema, { cardId: 1 }); + const data = create(Data.Event_AttachCardSchema, { cardId: 1 }); Dispatch.cardAttached(1, 2, data); expect(store.dispatch).toHaveBeenCalledWith(Actions.cardAttached(1, 2, data)); }); it('tokenCreated dispatches Actions.tokenCreated()', () => { - const data = create(Event_CreateTokenSchema, { cardId: 1 }); + const data = create(Data.Event_CreateTokenSchema, { cardId: 1 }); Dispatch.tokenCreated(1, 2, data); expect(store.dispatch).toHaveBeenCalledWith(Actions.tokenCreated(1, 2, data)); }); it('cardAttrChanged dispatches Actions.cardAttrChanged()', () => { - const data = create(Event_SetCardAttrSchema, { cardId: 1 }); + const data = create(Data.Event_SetCardAttrSchema, { cardId: 1 }); Dispatch.cardAttrChanged(1, 2, data); expect(store.dispatch).toHaveBeenCalledWith(Actions.cardAttrChanged(1, 2, data)); }); it('cardCounterChanged dispatches Actions.cardCounterChanged()', () => { - const data = create(Event_SetCardCounterSchema, { cardId: 1 }); + const data = create(Data.Event_SetCardCounterSchema, { cardId: 1 }); Dispatch.cardCounterChanged(1, 2, data); expect(store.dispatch).toHaveBeenCalledWith(Actions.cardCounterChanged(1, 2, data)); }); it('arrowCreated dispatches Actions.arrowCreated()', () => { - const data = create(Event_CreateArrowSchema, { arrowInfo: makeArrow() }); + const data = create(Data.Event_CreateArrowSchema, { arrowInfo: makeArrow() }); Dispatch.arrowCreated(1, 2, data); expect(store.dispatch).toHaveBeenCalledWith(Actions.arrowCreated(1, 2, data)); }); it('arrowDeleted dispatches Actions.arrowDeleted()', () => { - const data = create(Event_DeleteArrowSchema, { arrowId: 3 }); + const data = create(Data.Event_DeleteArrowSchema, { arrowId: 3 }); Dispatch.arrowDeleted(1, 2, data); expect(store.dispatch).toHaveBeenCalledWith(Actions.arrowDeleted(1, 2, data)); }); it('counterCreated dispatches Actions.counterCreated()', () => { - const data = create(Event_CreateCounterSchema, { counterInfo: makeCounter() }); + const data = create(Data.Event_CreateCounterSchema, { counterInfo: makeCounter() }); Dispatch.counterCreated(1, 2, data); expect(store.dispatch).toHaveBeenCalledWith(Actions.counterCreated(1, 2, data)); }); it('counterSet dispatches Actions.counterSet()', () => { - const data = create(Event_SetCounterSchema, { counterId: 1, value: 10 }); + const data = create(Data.Event_SetCounterSchema, { counterId: 1, value: 10 }); Dispatch.counterSet(1, 2, data); expect(store.dispatch).toHaveBeenCalledWith(Actions.counterSet(1, 2, data)); }); it('counterDeleted dispatches Actions.counterDeleted()', () => { - const data = create(Event_DelCounterSchema, { counterId: 1 }); + const data = create(Data.Event_DelCounterSchema, { counterId: 1 }); Dispatch.counterDeleted(1, 2, data); expect(store.dispatch).toHaveBeenCalledWith(Actions.counterDeleted(1, 2, data)); }); it('cardsDrawn dispatches Actions.cardsDrawn()', () => { - const data = create(Event_DrawCardsSchema, { number: 2, cards: [makeCard()] }); + const data = create(Data.Event_DrawCardsSchema, { number: 2, cards: [makeCard()] }); Dispatch.cardsDrawn(1, 2, data); expect(store.dispatch).toHaveBeenCalledWith(Actions.cardsDrawn(1, 2, data)); }); it('cardsRevealed dispatches Actions.cardsRevealed()', () => { - const data = create(Event_RevealCardsSchema, { zoneName: 'hand', cards: [] }); + const data = create(Data.Event_RevealCardsSchema, { zoneName: 'hand', cards: [] }); Dispatch.cardsRevealed(1, 2, data); expect(store.dispatch).toHaveBeenCalledWith(Actions.cardsRevealed(1, 2, data)); }); it('zoneShuffled dispatches Actions.zoneShuffled()', () => { - const data = create(Event_ShuffleSchema, { zoneName: 'deck', start: 0, end: 39 }); + const data = create(Data.Event_ShuffleSchema, { zoneName: 'deck', start: 0, end: 39 }); Dispatch.zoneShuffled(1, 2, data); expect(store.dispatch).toHaveBeenCalledWith(Actions.zoneShuffled(1, 2, data)); }); it('dieRolled dispatches Actions.dieRolled()', () => { - const data = create(Event_RollDieSchema, { sides: 6, value: 4, values: [4] }); + const data = create(Data.Event_RollDieSchema, { sides: 6, value: 4, values: [4] }); Dispatch.dieRolled(1, 2, data); expect(store.dispatch).toHaveBeenCalledWith(Actions.dieRolled(1, 2, data)); }); @@ -202,13 +181,13 @@ describe('Dispatch', () => { }); it('zoneDumped dispatches Actions.zoneDumped()', () => { - const data = create(Event_DumpZoneSchema, { zoneOwnerId: 1, zoneName: 'hand', numberCards: 3, isReversed: false }); + const data = create(Data.Event_DumpZoneSchema, { zoneOwnerId: 1, zoneName: 'hand', numberCards: 3, isReversed: false }); Dispatch.zoneDumped(1, 2, data); expect(store.dispatch).toHaveBeenCalledWith(Actions.zoneDumped(1, 2, data)); }); it('zonePropertiesChanged dispatches Actions.zonePropertiesChanged()', () => { - const data = create(Event_ChangeZonePropertiesSchema, { zoneName: 'deck', alwaysRevealTopCard: true, alwaysLookAtTopCard: false }); + const data = create(Data.Event_ChangeZonePropertiesSchema, { zoneName: 'deck', alwaysRevealTopCard: true, alwaysLookAtTopCard: false }); Dispatch.zonePropertiesChanged(1, 2, data); expect(store.dispatch).toHaveBeenCalledWith(Actions.zonePropertiesChanged(1, 2, data)); }); diff --git a/webclient/src/store/game/game.dispatch.ts b/webclient/src/store/game/game.dispatch.ts index ea0dbcfff..413b2b2e6 100644 --- a/webclient/src/store/game/game.dispatch.ts +++ b/webclient/src/store/game/game.dispatch.ts @@ -1,34 +1,14 @@ -import type { Event_AttachCard } from 'generated/proto/event_attach_card_pb'; -import type { Event_ChangeZoneProperties } from 'generated/proto/event_change_zone_properties_pb'; -import type { Event_CreateArrow } from 'generated/proto/event_create_arrow_pb'; -import type { Event_CreateCounter } from 'generated/proto/event_create_counter_pb'; -import type { Event_CreateToken } from 'generated/proto/event_create_token_pb'; -import type { Event_DelCounter } from 'generated/proto/event_del_counter_pb'; -import type { Event_DeleteArrow } from 'generated/proto/event_delete_arrow_pb'; -import type { Event_DestroyCard } from 'generated/proto/event_destroy_card_pb'; -import type { Event_DrawCards } from 'generated/proto/event_draw_cards_pb'; -import type { Event_DumpZone } from 'generated/proto/event_dump_zone_pb'; -import type { Event_FlipCard } from 'generated/proto/event_flip_card_pb'; -import type { Event_GameStateChanged } from 'generated/proto/event_game_state_changed_pb'; -import type { Event_MoveCard } from 'generated/proto/event_move_card_pb'; -import type { ServerInfo_PlayerProperties } from 'generated/proto/serverinfo_playerproperties_pb'; -import type { Event_RevealCards } from 'generated/proto/event_reveal_cards_pb'; -import type { Event_RollDie } from 'generated/proto/event_roll_die_pb'; -import type { Event_SetCardAttr } from 'generated/proto/event_set_card_attr_pb'; -import type { Event_SetCardCounter } from 'generated/proto/event_set_card_counter_pb'; -import type { Event_SetCounter } from 'generated/proto/event_set_counter_pb'; -import type { Event_Shuffle } from 'generated/proto/event_shuffle_pb'; -import { store } from 'store/store'; +import type { Data } from '@app/types'; +import { store } from '..'; import { Actions } from './game.actions'; -import { GameEntry } from './game.interfaces'; export const Dispatch = { clearStore: () => { store.dispatch(Actions.clearStore()); }, - gameJoined: (gameId: number, gameEntry: GameEntry) => { - store.dispatch(Actions.gameJoined(gameId, gameEntry)); + gameJoined: (data: Data.Event_GameJoined) => { + store.dispatch(Actions.gameJoined(data)); }, gameLeft: (gameId: number) => { @@ -43,11 +23,11 @@ export const Dispatch = { store.dispatch(Actions.gameHostChanged(gameId, hostId)); }, - gameStateChanged: (gameId: number, data: Event_GameStateChanged) => { + gameStateChanged: (gameId: number, data: Data.Event_GameStateChanged) => { store.dispatch(Actions.gameStateChanged(gameId, data)); }, - playerJoined: (gameId: number, playerProperties: ServerInfo_PlayerProperties) => { + playerJoined: (gameId: number, playerProperties: Data.ServerInfo_PlayerProperties) => { store.dispatch(Actions.playerJoined(gameId, playerProperties)); }, @@ -55,7 +35,7 @@ export const Dispatch = { store.dispatch(Actions.playerLeft(gameId, playerId, reason)); }, - playerPropertiesChanged: (gameId: number, playerId: number, properties: ServerInfo_PlayerProperties) => { + playerPropertiesChanged: (gameId: number, playerId: number, properties: Data.ServerInfo_PlayerProperties) => { store.dispatch(Actions.playerPropertiesChanged(gameId, playerId, properties)); }, @@ -63,67 +43,67 @@ export const Dispatch = { store.dispatch(Actions.kicked(gameId)); }, - cardMoved: (gameId: number, playerId: number, data: Event_MoveCard) => { + cardMoved: (gameId: number, playerId: number, data: Data.Event_MoveCard) => { store.dispatch(Actions.cardMoved(gameId, playerId, data)); }, - cardFlipped: (gameId: number, playerId: number, data: Event_FlipCard) => { + cardFlipped: (gameId: number, playerId: number, data: Data.Event_FlipCard) => { store.dispatch(Actions.cardFlipped(gameId, playerId, data)); }, - cardDestroyed: (gameId: number, playerId: number, data: Event_DestroyCard) => { + cardDestroyed: (gameId: number, playerId: number, data: Data.Event_DestroyCard) => { store.dispatch(Actions.cardDestroyed(gameId, playerId, data)); }, - cardAttached: (gameId: number, playerId: number, data: Event_AttachCard) => { + cardAttached: (gameId: number, playerId: number, data: Data.Event_AttachCard) => { store.dispatch(Actions.cardAttached(gameId, playerId, data)); }, - tokenCreated: (gameId: number, playerId: number, data: Event_CreateToken) => { + tokenCreated: (gameId: number, playerId: number, data: Data.Event_CreateToken) => { store.dispatch(Actions.tokenCreated(gameId, playerId, data)); }, - cardAttrChanged: (gameId: number, playerId: number, data: Event_SetCardAttr) => { + cardAttrChanged: (gameId: number, playerId: number, data: Data.Event_SetCardAttr) => { store.dispatch(Actions.cardAttrChanged(gameId, playerId, data)); }, - cardCounterChanged: (gameId: number, playerId: number, data: Event_SetCardCounter) => { + cardCounterChanged: (gameId: number, playerId: number, data: Data.Event_SetCardCounter) => { store.dispatch(Actions.cardCounterChanged(gameId, playerId, data)); }, - arrowCreated: (gameId: number, playerId: number, data: Event_CreateArrow) => { + arrowCreated: (gameId: number, playerId: number, data: Data.Event_CreateArrow) => { store.dispatch(Actions.arrowCreated(gameId, playerId, data)); }, - arrowDeleted: (gameId: number, playerId: number, data: Event_DeleteArrow) => { + arrowDeleted: (gameId: number, playerId: number, data: Data.Event_DeleteArrow) => { store.dispatch(Actions.arrowDeleted(gameId, playerId, data)); }, - counterCreated: (gameId: number, playerId: number, data: Event_CreateCounter) => { + counterCreated: (gameId: number, playerId: number, data: Data.Event_CreateCounter) => { store.dispatch(Actions.counterCreated(gameId, playerId, data)); }, - counterSet: (gameId: number, playerId: number, data: Event_SetCounter) => { + counterSet: (gameId: number, playerId: number, data: Data.Event_SetCounter) => { store.dispatch(Actions.counterSet(gameId, playerId, data)); }, - counterDeleted: (gameId: number, playerId: number, data: Event_DelCounter) => { + counterDeleted: (gameId: number, playerId: number, data: Data.Event_DelCounter) => { store.dispatch(Actions.counterDeleted(gameId, playerId, data)); }, - cardsDrawn: (gameId: number, playerId: number, data: Event_DrawCards) => { + cardsDrawn: (gameId: number, playerId: number, data: Data.Event_DrawCards) => { store.dispatch(Actions.cardsDrawn(gameId, playerId, data)); }, - cardsRevealed: (gameId: number, playerId: number, data: Event_RevealCards) => { + cardsRevealed: (gameId: number, playerId: number, data: Data.Event_RevealCards) => { store.dispatch(Actions.cardsRevealed(gameId, playerId, data)); }, - zoneShuffled: (gameId: number, playerId: number, data: Event_Shuffle) => { + zoneShuffled: (gameId: number, playerId: number, data: Data.Event_Shuffle) => { store.dispatch(Actions.zoneShuffled(gameId, playerId, data)); }, - dieRolled: (gameId: number, playerId: number, data: Event_RollDie) => { + dieRolled: (gameId: number, playerId: number, data: Data.Event_RollDie) => { store.dispatch(Actions.dieRolled(gameId, playerId, data)); }, @@ -139,11 +119,11 @@ export const Dispatch = { store.dispatch(Actions.turnReversed(gameId, reversed)); }, - zoneDumped: (gameId: number, playerId: number, data: Event_DumpZone) => { + zoneDumped: (gameId: number, playerId: number, data: Data.Event_DumpZone) => { store.dispatch(Actions.zoneDumped(gameId, playerId, data)); }, - zonePropertiesChanged: (gameId: number, playerId: number, data: Event_ChangeZoneProperties) => { + zonePropertiesChanged: (gameId: number, playerId: number, data: Data.Event_ChangeZoneProperties) => { store.dispatch(Actions.zonePropertiesChanged(gameId, playerId, data)); }, diff --git a/webclient/src/store/game/game.interfaces.ts b/webclient/src/store/game/game.interfaces.ts index 62abb9774..62b87d991 100644 --- a/webclient/src/store/game/game.interfaces.ts +++ b/webclient/src/store/game/game.interfaces.ts @@ -1,7 +1,4 @@ -import type { ServerInfo_Card } from 'generated/proto/serverinfo_card_pb'; -import type { ServerInfo_Counter } from 'generated/proto/serverinfo_counter_pb'; -import type { ServerInfo_Arrow } from 'generated/proto/serverinfo_arrow_pb'; -import type { ServerInfo_PlayerProperties } from 'generated/proto/serverinfo_playerproperties_pb'; +import type { Data } from '@app/types'; export interface GamesState { games: { [gameId: number]: GameEntry }; @@ -32,14 +29,14 @@ export interface GameEntry { /** Normalized from ServerInfo_Player — keyed collections for O(1) lookup. */ export interface PlayerEntry { - properties: ServerInfo_PlayerProperties; + properties: Data.ServerInfo_PlayerProperties; deckList: string; /** Zones keyed by zone name (e.g. "hand", "deck", "table"). */ zones: { [zoneName: string]: ZoneEntry }; /** Player-level counters (e.g. life) keyed by counter id. */ - counters: { [counterId: number]: ServerInfo_Counter }; + counters: { [counterId: number]: Data.ServerInfo_Counter }; /** Arrows keyed by arrow id. */ - arrows: { [arrowId: number]: ServerInfo_Arrow }; + arrows: { [arrowId: number]: Data.ServerInfo_Arrow }; } /** Normalized from ServerInfo_Zone — card list is an ordered array matching proto. */ @@ -51,7 +48,7 @@ export interface ZoneEntry { /** Authoritative card count (used for hidden zones where cardList may be empty). */ cardCount: number; /** Ordered card list; may be empty for hidden zones with no dump active. */ - cards: ServerInfo_Card[]; + cards: Data.ServerInfo_Card[]; alwaysRevealTopCard: boolean; alwaysLookAtTopCard: boolean; } diff --git a/webclient/src/store/game/game.reducer.spec.ts b/webclient/src/store/game/game.reducer.spec.ts index bda9cae90..048dbbd93 100644 --- a/webclient/src/store/game/game.reducer.spec.ts +++ b/webclient/src/store/game/game.reducer.spec.ts @@ -1,6 +1,5 @@ import { create } from '@bufbuild/protobuf'; -import { CardAttribute } from 'generated/proto/card_attributes_pb'; -import type { ServerInfo_Player } from 'generated/proto/serverinfo_player_pb'; +import { Data } from '@app/types'; import { gamesReducer } from './game.reducer'; import { Types } from './game.types'; import { @@ -13,7 +12,6 @@ import { makeState, makeZoneEntry, } from './__mocks__/fixtures'; -import { ServerInfo_PlayerSchema } from 'generated/proto/serverinfo_player_pb'; // ── 2A: Initialisation & lifecycle ─────────────────────────────────────────── @@ -30,9 +28,16 @@ describe('2A: Initialisation & lifecycle', () => { }); it('GAME_JOINED → inserts gameEntry keyed by gameId', () => { - const entry = makeGameEntry({ gameId: 42 }); - const result = gamesReducer({ games: {} }, { type: Types.GAME_JOINED, gameId: 42, gameEntry: entry }); - expect(result.games[42]).toBe(entry); + const data = create(Data.Event_GameJoinedSchema, { + gameInfo: create(Data.ServerInfo_GameSchema, { gameId: 42, roomId: 1, description: 'test' }), + hostId: 5, + playerId: 2, + spectator: false, + judge: false, + resuming: false, + }); + const result = gamesReducer({ games: {} }, { type: Types.GAME_JOINED, data }); + expect(result.games[42]).toEqual(expect.objectContaining({ gameId: 42, hostId: 5, localPlayerId: 2 })); }); it('GAME_LEFT → removes game by gameId', () => { @@ -69,8 +74,8 @@ describe('2B: Game state & player management', () => { const card = makeCard({ id: 5 }); const counter = makeCounter({ id: 2 }); const arrow = makeArrow({ id: 3 }); - const playerList: ServerInfo_Player[] = [ - create(ServerInfo_PlayerSchema, { + const playerList: Data.ServerInfo_Player[] = [ + create(Data.ServerInfo_PlayerSchema, { properties: makePlayerProperties({ playerId: 7 }), deckList: 'some deck', zoneList: [ @@ -550,7 +555,7 @@ describe('2E: CARD_ATTR_CHANGED', () => { }); } - function dispatchAttr(state: ReturnType, attribute: CardAttribute, attrValue: string) { + function dispatchAttr(state: ReturnType, attribute: Data.CardAttribute, attrValue: string) { return gamesReducer(state, { type: Types.CARD_ATTR_CHANGED, gameId: 1, @@ -560,37 +565,37 @@ describe('2E: CARD_ATTR_CHANGED', () => { } it('AttrTapped (1) → card.tapped = true when attrValue is "1"', () => { - const result = dispatchAttr(stateWithCard(), CardAttribute.AttrTapped, '1'); + const result = dispatchAttr(stateWithCard(), Data.CardAttribute.AttrTapped, '1'); expect(result.games[1].players[1].zones['table'].cards[0].tapped).toBe(true); }); it('AttrAttacking (2) → card.attacking = true when attrValue is "1"', () => { - const result = dispatchAttr(stateWithCard(), CardAttribute.AttrAttacking, '1'); + const result = dispatchAttr(stateWithCard(), Data.CardAttribute.AttrAttacking, '1'); expect(result.games[1].players[1].zones['table'].cards[0].attacking).toBe(true); }); it('AttrFaceDown (3) → card.faceDown = true when attrValue is "1"', () => { - const result = dispatchAttr(stateWithCard(), CardAttribute.AttrFaceDown, '1'); + const result = dispatchAttr(stateWithCard(), Data.CardAttribute.AttrFaceDown, '1'); expect(result.games[1].players[1].zones['table'].cards[0].faceDown).toBe(true); }); it('AttrColor (4) → card.color = attrValue', () => { - const result = dispatchAttr(stateWithCard(), CardAttribute.AttrColor, 'red'); + const result = dispatchAttr(stateWithCard(), Data.CardAttribute.AttrColor, 'red'); expect(result.games[1].players[1].zones['table'].cards[0].color).toBe('red'); }); it('AttrPT (5) → card.pt = attrValue', () => { - const result = dispatchAttr(stateWithCard(), CardAttribute.AttrPT, '2/3'); + const result = dispatchAttr(stateWithCard(), Data.CardAttribute.AttrPT, '2/3'); expect(result.games[1].players[1].zones['table'].cards[0].pt).toBe('2/3'); }); it('AttrAnnotation (6) → card.annotation = attrValue', () => { - const result = dispatchAttr(stateWithCard(), CardAttribute.AttrAnnotation, 'enchanted'); + const result = dispatchAttr(stateWithCard(), Data.CardAttribute.AttrAnnotation, 'enchanted'); expect(result.games[1].players[1].zones['table'].cards[0].annotation).toBe('enchanted'); }); it('AttrDoesntUntap (7) → card.doesntUntap = true when attrValue is "1"', () => { - const result = dispatchAttr(stateWithCard(), CardAttribute.AttrDoesntUntap, '1'); + const result = dispatchAttr(stateWithCard(), Data.CardAttribute.AttrDoesntUntap, '1'); expect(result.games[1].players[1].zones['table'].cards[0].doesntUntap).toBe(true); }); }); diff --git a/webclient/src/store/game/game.reducer.ts b/webclient/src/store/game/game.reducer.ts index 451b75fd2..602a0dbbb 100644 --- a/webclient/src/store/game/game.reducer.ts +++ b/webclient/src/store/game/game.reducer.ts @@ -1,12 +1,5 @@ -import { CardAttribute } from 'generated/proto/card_attributes_pb'; -import type { ServerInfo_CardCounter } from 'generated/proto/serverinfo_cardcounter_pb'; -import type { ServerInfo_Card } from 'generated/proto/serverinfo_card_pb'; -import type { ServerInfo_Counter } from 'generated/proto/serverinfo_counter_pb'; -import type { ServerInfo_Arrow } from 'generated/proto/serverinfo_arrow_pb'; -import type { ServerInfo_Player } from 'generated/proto/serverinfo_player_pb'; +import { Data } from '@app/types'; import { create } from '@bufbuild/protobuf'; -import { ServerInfo_CardSchema } from 'generated/proto/serverinfo_card_pb'; -import { ServerInfo_CardCounterSchema } from 'generated/proto/serverinfo_cardcounter_pb'; import { GameAction } from './game.actions'; import { GameEntry, GameMessage, GamesState, PlayerEntry, ZoneEntry } from './game.interfaces'; import { Types } from './game.types'; @@ -74,7 +67,7 @@ function removeGame(state: GamesState, gameId: number): GamesState { } /** Converts the proto PlayerInfo[] array into the keyed PlayerEntry map used in the store. */ -function normalizePlayers(playerList: ServerInfo_Player[]): { [playerId: number]: PlayerEntry } { +function normalizePlayers(playerList: Data.ServerInfo_Player[]): { [playerId: number]: PlayerEntry } { const players: { [playerId: number]: PlayerEntry } = {}; for (const player of playerList) { const playerId = player.properties.playerId; @@ -92,12 +85,12 @@ function normalizePlayers(playerList: ServerInfo_Player[]): { [playerId: number] }; } - const counters: { [counterId: number]: ServerInfo_Counter } = {}; + const counters: { [counterId: number]: Data.ServerInfo_Counter } = {}; for (const counter of player.counterList) { counters[counter.id] = counter; } - const arrows: { [arrowId: number]: ServerInfo_Arrow } = {}; + const arrows: { [arrowId: number]: Data.ServerInfo_Arrow } = {}; for (const arrow of player.arrowList) { arrows[arrow.id] = arrow; } @@ -120,8 +113,8 @@ function buildEmptyCard( y: number, faceDown: boolean, providerId: string -): ServerInfo_Card { - return create(ServerInfo_CardSchema, { +): Data.ServerInfo_Card { + return create(Data.ServerInfo_CardSchema, { id, name, x, @@ -157,9 +150,31 @@ export const gamesReducer = (state: GamesState = initialState, action: GameActio } case Types.GAME_JOINED: { + const { data } = action; + const gameInfo = data.gameInfo; + if (!gameInfo) { + return state; + } + const gameEntry: GameEntry = { + gameId: gameInfo.gameId, + roomId: gameInfo.roomId, + description: gameInfo.description, + hostId: data.hostId, + localPlayerId: data.playerId, + spectator: data.spectator, + judge: data.judge, + resuming: data.resuming, + started: gameInfo.started, + activePlayerId: -1, + activePhase: -1, + secondsElapsed: 0, + reversed: false, + players: {}, + messages: [], + }; return { ...state, - games: { ...state.games, [action.gameId]: action.gameEntry }, + games: { ...state.games, [gameEntry.gameId]: gameEntry }, }; } @@ -266,8 +281,8 @@ export const gamesReducer = (state: GamesState = initialState, action: GameActio } // Locate card in source zone (by id for visible zones, by position for hidden) - let removedCard: ServerInfo_Card | undefined; - let newSourceCards: ServerInfo_Card[]; + let removedCard: Data.ServerInfo_Card | undefined; + let newSourceCards: Data.ServerInfo_Card[]; if (cardId >= 0) { removedCard = sourceZoneEntry.cards.find(c => c.id === cardId); newSourceCards = sourceZoneEntry.cards.filter(c => c.id !== cardId); @@ -280,7 +295,7 @@ export const gamesReducer = (state: GamesState = initialState, action: GameActio } const effectiveNewId = newCardId >= 0 ? newCardId : (removedCard?.id ?? -1); - const movedCard: ServerInfo_Card = removedCard + const movedCard: Data.ServerInfo_Card = removedCard ? { ...removedCard, id: effectiveNewId, @@ -423,7 +438,7 @@ export const gamesReducer = (state: GamesState = initialState, action: GameActio return state; } - const newCard: ServerInfo_Card = create(ServerInfo_CardSchema, { + const newCard: Data.ServerInfo_Card = create(Data.ServerInfo_CardSchema, { id: cardId, name: cardName, x, @@ -469,15 +484,15 @@ export const gamesReducer = (state: GamesState = initialState, action: GameActio return state; } - const attrPatch: Partial = {}; - switch (attribute as CardAttribute) { - case CardAttribute.AttrTapped: attrPatch.tapped = attrValue === '1'; break; - case CardAttribute.AttrAttacking: attrPatch.attacking = attrValue === '1'; break; - case CardAttribute.AttrFaceDown: attrPatch.faceDown = attrValue === '1'; break; - case CardAttribute.AttrColor: attrPatch.color = attrValue; break; - case CardAttribute.AttrPT: attrPatch.pt = attrValue; break; - case CardAttribute.AttrAnnotation: attrPatch.annotation = attrValue; break; - case CardAttribute.AttrDoesntUntap: attrPatch.doesntUntap = attrValue === '1'; break; + const attrPatch: Partial = {}; + switch (attribute as Data.CardAttribute) { + case Data.CardAttribute.AttrTapped: attrPatch.tapped = attrValue === '1'; break; + case Data.CardAttribute.AttrAttacking: attrPatch.attacking = attrValue === '1'; break; + case Data.CardAttribute.AttrFaceDown: attrPatch.faceDown = attrValue === '1'; break; + case Data.CardAttribute.AttrColor: attrPatch.color = attrValue; break; + case Data.CardAttribute.AttrPT: attrPatch.pt = attrValue; break; + case Data.CardAttribute.AttrAnnotation: attrPatch.annotation = attrValue; break; + case Data.CardAttribute.AttrDoesntUntap: attrPatch.doesntUntap = attrValue === '1'; break; } const updatedCards = [...zone.cards]; @@ -507,7 +522,7 @@ export const gamesReducer = (state: GamesState = initialState, action: GameActio } const card = zone.cards[cardIdx]; - let newCounterList: ServerInfo_CardCounter[]; + let newCounterList: Data.ServerInfo_CardCounter[]; if (counterValue <= 0) { newCounterList = card.counterList.filter(c => c.id !== counterId); } else { @@ -515,7 +530,7 @@ export const gamesReducer = (state: GamesState = initialState, action: GameActio newCounterList = existing >= 0 ? card.counterList.map(c => (c.id === counterId ? { ...c, value: counterValue } : c)) - : [...card.counterList, create(ServerInfo_CardCounterSchema, { id: counterId, value: counterValue })]; + : [...card.counterList, create(Data.ServerInfo_CardCounterSchema, { id: counterId, value: counterValue })]; } const updatedCards = [...zone.cards]; diff --git a/webclient/src/store/game/game.selectors.ts b/webclient/src/store/game/game.selectors.ts index acd653039..179c6de82 100644 --- a/webclient/src/store/game/game.selectors.ts +++ b/webclient/src/store/game/game.selectors.ts @@ -1,12 +1,12 @@ import { createSelector } from '@reduxjs/toolkit'; -import type { ServerInfo_Card } from 'generated/proto/serverinfo_card_pb'; +import type { Data } from '@app/types'; import { GamesState, GameEntry, PlayerEntry, ZoneEntry } from './game.interfaces'; interface State { games: GamesState; } -const EMPTY_ARRAY: ServerInfo_Card[] = []; +const EMPTY_ARRAY: Data.ServerInfo_Card[] = []; const EMPTY_OBJECT = {} as Record; export const Selectors = { diff --git a/webclient/src/store/index.ts b/webclient/src/store/index.ts index 2ee14345d..701e6a362 100644 --- a/webclient/src/store/index.ts +++ b/webclient/src/store/index.ts @@ -9,7 +9,7 @@ export { Selectors as GameSelectors, Dispatch as GameDispatch } from './game'; -export * from 'store/game/game.interfaces'; +export * from './game/game.interfaces'; // Server export { @@ -17,13 +17,13 @@ export { Selectors as ServerSelectors, Dispatch as ServerDispatch } from './server'; -export * from 'store/server/server.interfaces'; +export * from './server/server.interfaces'; export { Types as RoomsTypes, Selectors as RoomsSelectors, - Dispatch as RoomsDispatch } from 'store/rooms'; + Dispatch as RoomsDispatch } from './rooms'; -export * from 'store/rooms/rooms.interfaces'; +export * from './rooms/rooms.interfaces'; diff --git a/webclient/src/store/rooms/__mocks__/rooms-fixtures.ts b/webclient/src/store/rooms/__mocks__/rooms-fixtures.ts index 311980a98..940ec76ea 100644 --- a/webclient/src/store/rooms/__mocks__/rooms-fixtures.ts +++ b/webclient/src/store/rooms/__mocks__/rooms-fixtures.ts @@ -1,21 +1,13 @@ -import { - Game, - GameSortField, - Message, - ProtoInit, - Room, - SortDirection, - UserSortField, -} from 'types'; -import type { ServerInfo_User } from 'generated/proto/serverinfo_user_pb'; +import { App, Data, Enriched } from '@app/types'; +import type { MessageInitShape } from '@bufbuild/protobuf'; + import { create } from '@bufbuild/protobuf'; -import { ServerInfo_UserSchema } from 'generated/proto/serverinfo_user_pb'; -import { ServerInfo_GameSchema } from 'generated/proto/serverinfo_game_pb'; -import { ServerInfo_RoomSchema } from 'generated/proto/serverinfo_room_pb'; import { RoomsState } from '../rooms.interfaces'; -export function makeUser(overrides: ProtoInit = {}): ServerInfo_User { - return create(ServerInfo_UserSchema, { +export function makeUser( + overrides: MessageInitShape = {} +): Data.ServerInfo_User { + return create(Data.ServerInfo_UserSchema, { name: 'TestUser', accountageSecs: 0n, privlevel: '', @@ -24,10 +16,10 @@ export function makeUser(overrides: ProtoInit = {}): ServerInfo }); } -export function makeRoom(overrides: ProtoInit = {}): Room { +export function makeRoom(overrides: Partial> = {}): Enriched.Room { const { gametypeMap = {}, order = 0, gameList = [], ...protoOverrides } = overrides; return { - ...create(ServerInfo_RoomSchema, { + ...create(Data.ServerInfo_RoomSchema, { roomId: 1, name: 'Test Room', description: '', @@ -45,10 +37,12 @@ export function makeRoom(overrides: ProtoInit = {}): Room { }; } -export function makeGame(overrides: ProtoInit = {}): Game & { startTime: number } { +export function makeGame( + overrides: Partial> = {}, +): Enriched.Game & { startTime: number } { const { gameType = '', startTime = 0, ...protoOverrides } = overrides; return { - ...create(ServerInfo_GameSchema, { + ...create(Data.ServerInfo_GameSchema, { gameId: 1, roomId: 1, description: 'Test Game', @@ -61,7 +55,7 @@ export function makeGame(overrides: ProtoInit = {} }; } -export function makeMessage(overrides: Partial = {}): Message { +export function makeMessage(overrides: Partial = {}): Enriched.Message { return { message: 'hello', messageType: 0, @@ -80,12 +74,12 @@ export function makeRoomsState(overrides: Partial = {}): RoomsState joinedGameIds: {}, messages: {}, sortGamesBy: { - field: GameSortField.START_TIME, - order: SortDirection.DESC, + field: App.GameSortField.START_TIME, + order: App.SortDirection.DESC, }, sortUsersBy: { - field: UserSortField.NAME, - order: SortDirection.ASC, + field: App.UserSortField.NAME, + order: App.SortDirection.ASC, }, ...overrides, }; diff --git a/webclient/src/store/rooms/rooms.actions.spec.ts b/webclient/src/store/rooms/rooms.actions.spec.ts index 542d626b9..cd8d3a936 100644 --- a/webclient/src/store/rooms/rooms.actions.spec.ts +++ b/webclient/src/store/rooms/rooms.actions.spec.ts @@ -1,7 +1,7 @@ import { Actions } from './rooms.actions'; import { Types } from './rooms.types'; import { makeGame, makeMessage, makeRoom, makeUser } from './__mocks__/rooms-fixtures'; -import { GameSortField, SortDirection } from 'types'; +import { App } from '@app/types'; describe('Actions', () => { it('clearStore', () => { @@ -42,11 +42,11 @@ describe('Actions', () => { }); it('sortGames', () => { - expect(Actions.sortGames(1, GameSortField.START_TIME, SortDirection.ASC)).toEqual({ + expect(Actions.sortGames(1, App.GameSortField.START_TIME, App.SortDirection.ASC)).toEqual({ type: Types.SORT_GAMES, roomId: 1, - field: GameSortField.START_TIME, - order: SortDirection.ASC, + field: App.GameSortField.START_TIME, + order: App.SortDirection.ASC, }); }); diff --git a/webclient/src/store/rooms/rooms.actions.tsx b/webclient/src/store/rooms/rooms.actions.tsx index 40dfa7559..df4acac95 100644 --- a/webclient/src/store/rooms/rooms.actions.tsx +++ b/webclient/src/store/rooms/rooms.actions.tsx @@ -1,7 +1,4 @@ -import { GameSortField, Message, SortDirection } from 'types'; -import type { ServerInfo_User } from 'generated/proto/serverinfo_user_pb'; -import type { ServerInfo_Room } from 'generated/proto/serverinfo_room_pb'; -import type { ServerInfo_Game } from 'generated/proto/serverinfo_game_pb'; +import { App, Data, Enriched } from '@app/types'; import { Types } from './rooms.types'; @@ -10,12 +7,12 @@ export const Actions = { type: Types.CLEAR_STORE, }), - updateRooms: (rooms: ServerInfo_Room[]) => ({ + updateRooms: (rooms: Data.ServerInfo_Room[]) => ({ type: Types.UPDATE_ROOMS, rooms, }), - joinRoom: (roomInfo: ServerInfo_Room) => ({ + joinRoom: (roomInfo: Data.ServerInfo_Room) => ({ type: Types.JOIN_ROOM, roomInfo, }), @@ -25,19 +22,19 @@ export const Actions = { roomId, }), - addMessage: (roomId: number, message: Message) => ({ + addMessage: (roomId: number, message: Enriched.Message) => ({ type: Types.ADD_MESSAGE, roomId, message, }), - updateGames: (roomId: number, games: ServerInfo_Game[]) => ({ + updateGames: (roomId: number, games: Data.ServerInfo_Game[]) => ({ type: Types.UPDATE_GAMES, roomId, games, }), - userJoined: (roomId: number, user: ServerInfo_User) => ({ + userJoined: (roomId: number, user: Data.ServerInfo_User) => ({ type: Types.USER_JOINED, roomId, user, @@ -49,7 +46,7 @@ export const Actions = { name, }), - sortGames: (roomId: number, field: GameSortField, order: SortDirection) => ({ + sortGames: (roomId: number, field: App.GameSortField, order: App.SortDirection) => ({ type: Types.SORT_GAMES, roomId, field, diff --git a/webclient/src/store/rooms/rooms.dispatch.spec.ts b/webclient/src/store/rooms/rooms.dispatch.spec.ts index ff090b36c..9b305c894 100644 --- a/webclient/src/store/rooms/rooms.dispatch.spec.ts +++ b/webclient/src/store/rooms/rooms.dispatch.spec.ts @@ -1,12 +1,10 @@ -vi.mock('store', () => ({ store: { dispatch: vi.fn() } })); +vi.mock('..', () => ({ store: { dispatch: vi.fn() } })); -import { store } from 'store'; +import { store } from '..'; import { Actions } from './rooms.actions'; import { Dispatch } from './rooms.dispatch'; import { makeGame, makeMessage, makeRoom, makeUser } from './__mocks__/rooms-fixtures'; -import { GameSortField, SortDirection } from 'types'; - -beforeEach(() => vi.clearAllMocks()); +import { App } from '@app/types'; describe('Dispatch', () => { it('clearStore dispatches Actions.clearStore()', () => { @@ -63,9 +61,9 @@ describe('Dispatch', () => { }); it('sortGames dispatches Actions.sortGames()', () => { - Dispatch.sortGames(1, GameSortField.START_TIME, SortDirection.ASC); + Dispatch.sortGames(1, App.GameSortField.START_TIME, App.SortDirection.ASC); expect(store.dispatch).toHaveBeenCalledWith( - Actions.sortGames(1, GameSortField.START_TIME, SortDirection.ASC) + Actions.sortGames(1, App.GameSortField.START_TIME, App.SortDirection.ASC) ); }); diff --git a/webclient/src/store/rooms/rooms.dispatch.tsx b/webclient/src/store/rooms/rooms.dispatch.tsx index a1c653f12..efa7177cf 100644 --- a/webclient/src/store/rooms/rooms.dispatch.tsx +++ b/webclient/src/store/rooms/rooms.dispatch.tsx @@ -1,21 +1,18 @@ -import { GameSortField, Message, SortDirection } from 'types'; -import type { ServerInfo_User } from 'generated/proto/serverinfo_user_pb'; -import type { ServerInfo_Room } from 'generated/proto/serverinfo_room_pb'; -import type { ServerInfo_Game } from 'generated/proto/serverinfo_game_pb'; +import { App, Data, Enriched } from '@app/types'; import { Actions } from './rooms.actions'; -import { store } from 'store'; +import { store } from '..'; export const Dispatch = { clearStore: () => { store.dispatch(Actions.clearStore()); }, - updateRooms: (rooms: ServerInfo_Room[]) => { + updateRooms: (rooms: Data.ServerInfo_Room[]) => { store.dispatch(Actions.updateRooms(rooms)); }, - joinRoom: (roomInfo: ServerInfo_Room) => { + joinRoom: (roomInfo: Data.ServerInfo_Room) => { store.dispatch(Actions.joinRoom(roomInfo)); }, @@ -24,15 +21,15 @@ export const Dispatch = { store.dispatch(Actions.leaveRoom(roomId)); }, - addMessage: (roomId: number, message: Message) => { + addMessage: (roomId: number, message: Enriched.Message) => { store.dispatch(Actions.addMessage(roomId, message)); }, - updateGames: (roomId: number, games: ServerInfo_Game[]) => { + updateGames: (roomId: number, games: Data.ServerInfo_Game[]) => { store.dispatch(Actions.updateGames(roomId, games)); }, - userJoined: (roomId: number, user: ServerInfo_User) => { + userJoined: (roomId: number, user: Data.ServerInfo_User) => { store.dispatch(Actions.userJoined(roomId, user)); }, @@ -40,7 +37,7 @@ export const Dispatch = { store.dispatch(Actions.userLeft(roomId, name)); }, - sortGames: (roomId: number, field: GameSortField, order: SortDirection) => { + sortGames: (roomId: number, field: App.GameSortField, order: App.SortDirection) => { store.dispatch(Actions.sortGames(roomId, field, order)); }, diff --git a/webclient/src/store/rooms/rooms.interfaces.tsx b/webclient/src/store/rooms/rooms.interfaces.tsx index 8e9cf1943..bb1ba2375 100644 --- a/webclient/src/store/rooms/rooms.interfaces.tsx +++ b/webclient/src/store/rooms/rooms.interfaces.tsx @@ -1,4 +1,4 @@ -import { GameSortField, Message, Room, Game, SortBy, UserSortField } from 'types'; +import { App, Enriched } from '@app/types'; export interface RoomsState { rooms: RoomsStateRooms; @@ -11,12 +11,12 @@ export interface RoomsState { } export interface RoomsStateRooms { - [roomId: number]: Room; + [roomId: number]: Enriched.Room; } export interface RoomsStateGames { [roomId: number]: { - [gameId: number]: Game; + [gameId: number]: Enriched.Game; }; } @@ -31,13 +31,13 @@ export interface JoinedGames { } export interface RoomsStateMessages { - [roomId: number]: Message[]; + [roomId: number]: Enriched.Message[]; } -export interface RoomsStateSortGamesBy extends SortBy { - field: GameSortField +export interface RoomsStateSortGamesBy extends App.SortBy { + field: App.GameSortField } -export interface RoomsStateSortUsersBy extends SortBy { - field: UserSortField +export interface RoomsStateSortUsersBy extends App.SortBy { + field: App.UserSortField } diff --git a/webclient/src/store/rooms/rooms.reducer.spec.ts b/webclient/src/store/rooms/rooms.reducer.spec.ts index df0ef2836..874888944 100644 --- a/webclient/src/store/rooms/rooms.reducer.spec.ts +++ b/webclient/src/store/rooms/rooms.reducer.spec.ts @@ -1,4 +1,4 @@ -import { GameSortField, SortDirection } from 'types'; +import { App } from '@app/types'; import { roomsReducer } from './rooms.reducer'; import { Types, MAX_ROOM_MESSAGES } from './rooms.types'; import { makeGame, makeMessage, makeRoom, makeRoomsState, makeUser } from './__mocks__/rooms-fixtures'; @@ -191,11 +191,10 @@ describe('UPDATE_GAMES', () => { expect(result.rooms[1].gameList.find(g => g.gameId === 2).description).toBe('new'); }); - it('returns { ...state } (not identity) when roomId is unknown', () => { + it('returns state identity when roomId is unknown', () => { const state = makeRoomsState({ rooms: {} }); const result = roomsReducer(state, { type: Types.UPDATE_GAMES, roomId: 999, games: [] }); - expect(result).not.toBe(state); - expect(result.rooms).toEqual(state.rooms); + expect(result).toBe(state); }); }); @@ -231,10 +230,10 @@ describe('SORT_GAMES', () => { const result = roomsReducer(state, { type: Types.SORT_GAMES, roomId: 1, - field: GameSortField.START_TIME, - order: SortDirection.ASC, + field: App.GameSortField.START_TIME, + order: App.SortDirection.ASC, }); - expect(result.sortGamesBy).toEqual({ field: GameSortField.START_TIME, order: SortDirection.ASC }); + expect(result.sortGamesBy).toEqual({ field: App.GameSortField.START_TIME, order: App.SortDirection.ASC }); }); }); diff --git a/webclient/src/store/rooms/rooms.reducer.tsx b/webclient/src/store/rooms/rooms.reducer.tsx index bbf5a1ed5..ec57b2255 100644 --- a/webclient/src/store/rooms/rooms.reducer.tsx +++ b/webclient/src/store/rooms/rooms.reducer.tsx @@ -1,6 +1,6 @@ import * as _ from 'lodash'; -import { GameSortField, Room, UserSortField, SortDirection } from 'types'; +import { App, Enriched } from '@app/types'; import { normalizeGameObject, normalizeGametypeMap, normalizeRoomInfo, normalizeUserMessage, SortUtil } from '../common'; @@ -15,12 +15,12 @@ const initialState: RoomsState = { joinedGameIds: {}, messages: {}, sortGamesBy: { - field: GameSortField.START_TIME, - order: SortDirection.DESC + field: App.GameSortField.START_TIME, + order: App.SortDirection.DESC }, sortUsersBy: { - field: UserSortField.NAME, - order: SortDirection.ASC + field: App.UserSortField.NAME, + order: App.SortDirection.ASC } }; @@ -46,11 +46,11 @@ export const roomsReducer = (state = initialState, action: RoomsAction) => { const gametypeMap = normalizeGametypeMap(gametypeList); rooms[roomId] = { - ...(existing as Room), + ...(existing as Enriched.Room), ...roomMeta, gametypeMap, - gameList: (existing as Room).gameList, - userList: (existing as Room).userList, + gameList: (existing as Enriched.Room).gameList, + userList: (existing as Enriched.Room).userList, order, }; }); @@ -149,8 +149,10 @@ export const roomsReducer = (state = initialState, action: RoomsAction) => { const { rooms, sortGamesBy } = state; const room = rooms[roomId]; - if (!room) { - return { ...state }; + // An empty gameList means no game updates — skip to avoid + // overwriting the existing game list with an empty one. + if (!room || !games?.length) { + return state; } // Normalize incoming raw proto games using the room's gametypeMap diff --git a/webclient/src/store/server/__mocks__/server-fixtures.ts b/webclient/src/store/server/__mocks__/server-fixtures.ts index 7a5cf20fb..5146db4c2 100644 --- a/webclient/src/store/server/__mocks__/server-fixtures.ts +++ b/webclient/src/store/server/__mocks__/server-fixtures.ts @@ -1,33 +1,13 @@ -import { - Game, - ProtoInit, - SortDirection, - StatusEnum, - UserSortField, - WebSocketConnectOptions, -} from 'types'; -import type { ServerInfo_User } from 'generated/proto/serverinfo_user_pb'; -import type { ServerInfo_Ban } from 'generated/proto/serverinfo_ban_pb'; -import type { ServerInfo_Warning } from 'generated/proto/serverinfo_warning_pb'; -import type { Response_WarnList } from 'generated/proto/response_warn_list_pb'; -import type { ServerInfo_ReplayMatch } from 'generated/proto/serverinfo_replay_match_pb'; -import type { ServerInfo_ChatMessage } from 'generated/proto/serverinfo_chat_message_pb'; -import type { Response_DeckList } from 'generated/proto/response_deck_list_pb'; -import type { ServerInfo_DeckStorage_TreeItem } from 'generated/proto/serverinfo_deckstorage_pb'; +import { App, Data, Enriched } from '@app/types'; +import type { MessageInitShape } from '@bufbuild/protobuf'; + import { create } from '@bufbuild/protobuf'; -import { ServerInfo_GameSchema } from 'generated/proto/serverinfo_game_pb'; -import { ServerInfo_UserSchema } from 'generated/proto/serverinfo_user_pb'; -import { ServerInfo_ReplayMatchSchema } from 'generated/proto/serverinfo_replay_match_pb'; -import { ServerInfo_ChatMessageSchema } from 'generated/proto/serverinfo_chat_message_pb'; -import { ServerInfo_BanSchema } from 'generated/proto/serverinfo_ban_pb'; -import { ServerInfo_WarningSchema } from 'generated/proto/serverinfo_warning_pb'; -import { Response_WarnListSchema } from 'generated/proto/response_warn_list_pb'; -import { ServerInfo_DeckStorage_TreeItemSchema, ServerInfo_DeckStorage_FolderSchema } from 'generated/proto/serverinfo_deckstorage_pb'; -import { Response_DeckListSchema } from 'generated/proto/response_deck_list_pb'; import { ServerState } from '../server.interfaces'; -export function makeUser(overrides: ProtoInit = {}): ServerInfo_User { - return create(ServerInfo_UserSchema, { +export function makeUser( + overrides: MessageInitShape = {} +): Data.ServerInfo_User { + return create(Data.ServerInfo_UserSchema, { name: 'TestUser', accountageSecs: 0n, privlevel: '', @@ -36,8 +16,10 @@ export function makeUser(overrides: ProtoInit = {}): ServerInfo }); } -export function makeLogItem(overrides: ProtoInit = {}): ServerInfo_ChatMessage { - return create(ServerInfo_ChatMessageSchema, { +export function makeLogItem( + overrides: MessageInitShape = {} +): Data.ServerInfo_ChatMessage { + return create(Data.ServerInfo_ChatMessageSchema, { message: '', senderId: '', senderIp: '', @@ -50,8 +32,10 @@ export function makeLogItem(overrides: ProtoInit = {}): }); } -export function makeBanHistoryItem(overrides: ProtoInit = {}): ServerInfo_Ban { - return create(ServerInfo_BanSchema, { +export function makeBanHistoryItem( + overrides: MessageInitShape = {} +): Data.ServerInfo_Ban { + return create(Data.ServerInfo_BanSchema, { adminId: '', adminName: '', banTime: '', @@ -62,8 +46,10 @@ export function makeBanHistoryItem(overrides: ProtoInit = {}): S }); } -export function makeWarnHistoryItem(overrides: ProtoInit = {}): ServerInfo_Warning { - return create(ServerInfo_WarningSchema, { +export function makeWarnHistoryItem( + overrides: MessageInitShape = {} +): Data.ServerInfo_Warning { + return create(Data.ServerInfo_WarningSchema, { userName: '', adminName: '', reason: '', @@ -72,8 +58,10 @@ export function makeWarnHistoryItem(overrides: ProtoInit = { }); } -export function makeWarnListItem(overrides: ProtoInit = {}): Response_WarnList { - return create(Response_WarnListSchema, { +export function makeWarnListItem( + overrides: MessageInitShape = {} +): Data.Response_WarnList { + return create(Data.Response_WarnListSchema, { warning: [], userName: '', userClientid: '', @@ -81,23 +69,29 @@ export function makeWarnListItem(overrides: ProtoInit = {}): }); } -export function makeDeckTreeItem(overrides: ProtoInit = {}): ServerInfo_DeckStorage_TreeItem { - return create(ServerInfo_DeckStorage_TreeItemSchema, { +export function makeDeckTreeItem( + overrides: MessageInitShape = {}, +): Data.ServerInfo_DeckStorage_TreeItem { + return create(Data.ServerInfo_DeckStorage_TreeItemSchema, { id: 1, name: 'item', ...overrides, }); } -export function makeDeckList(overrides: ProtoInit = {}): Response_DeckList { - return create(Response_DeckListSchema, { - root: create(ServerInfo_DeckStorage_FolderSchema, { items: [] }), +export function makeDeckList( + overrides: MessageInitShape = {} +): Data.Response_DeckList { + return create(Data.Response_DeckListSchema, { + root: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [] }), ...overrides, }); } -export function makeReplayMatch(overrides: ProtoInit = {}): ServerInfo_ReplayMatch { - return create(ServerInfo_ReplayMatchSchema, { +export function makeReplayMatch( + overrides: MessageInitShape = {} +): Data.ServerInfo_ReplayMatch { + return create(Data.ServerInfo_ReplayMatchSchema, { gameId: 1, roomName: 'Test Room', timeStarted: 0, @@ -110,16 +104,26 @@ export function makeReplayMatch(overrides: ProtoInit = { }); } -export function makeGame(overrides: Partial = {}): Game { - return { ...create(ServerInfo_GameSchema, { description: '' }), gameType: '', ...overrides }; +export function makeGame(overrides: Partial = {}): Enriched.Game { + return { ...create(Data.ServerInfo_GameSchema, { description: '' }), gameType: '', ...overrides }; } -export function makeConnectOptions(overrides: Partial = {}): WebSocketConnectOptions { +export function makeLoginSuccessContext( + overrides: Partial = {} +): Enriched.LoginSuccessContext { + return { + hashedPassword: 'hash', + ...overrides, + }; +} + +export function makePendingActivationContext( + overrides: Partial = {} +): Enriched.PendingActivationContext { return { host: 'localhost', port: '4747', userName: 'user', - password: 'pass', ...overrides, }; } @@ -131,7 +135,7 @@ export function makeServerState(overrides: Partial = {}): ServerSta ignoreList: [], status: { connectionAttemptMade: false, - state: StatusEnum.DISCONNECTED, + state: App.StatusEnum.DISCONNECTED, description: null, }, info: { @@ -147,8 +151,8 @@ export function makeServerState(overrides: Partial = {}): ServerSta user: null, users: [], sortUsersBy: { - field: UserSortField.NAME, - order: SortDirection.ASC, + field: App.UserSortField.NAME, + order: App.SortDirection.ASC, }, messages: {}, userInfo: {}, diff --git a/webclient/src/store/server/server.actions.spec.ts b/webclient/src/store/server/server.actions.spec.ts index b25e768f1..277ac816e 100644 --- a/webclient/src/store/server/server.actions.spec.ts +++ b/webclient/src/store/server/server.actions.spec.ts @@ -1,16 +1,14 @@ import { Actions } from './server.actions'; +import { App, Data } from '@app/types'; import { Types } from './server.types'; import { create } from '@bufbuild/protobuf'; -import { Event_NotifyUserSchema } from 'generated/proto/event_notify_user_pb'; -import { Event_ServerShutdownSchema } from 'generated/proto/event_server_shutdown_pb'; -import { Event_UserMessageSchema } from 'generated/proto/event_user_message_pb'; import { makeBanHistoryItem, - makeConnectOptions, + makeLoginSuccessContext, + makePendingActivationContext, makeDeckList, makeDeckTreeItem, makeReplayMatch, - makeGame, makeUser, makeWarnHistoryItem, makeWarnListItem, @@ -30,7 +28,7 @@ describe('Actions', () => { }); it('loginSuccessful', () => { - const options = makeConnectOptions(); + const options = makeLoginSuccessContext(); expect(Actions.loginSuccessful(options)).toEqual({ type: Types.LOGIN_SUCCESSFUL, options }); }); @@ -38,10 +36,6 @@ describe('Actions', () => { expect(Actions.loginFailed()).toEqual({ type: Types.LOGIN_FAILED }); }); - it('connectionClosed', () => { - expect(Actions.connectionClosed(3)).toEqual({ type: Types.CONNECTION_CLOSED, reason: 3 }); - }); - it('connectionFailed', () => { expect(Actions.connectionFailed()).toEqual({ type: Types.CONNECTION_FAILED }); }); @@ -92,7 +86,7 @@ describe('Actions', () => { }); it('updateStatus', () => { - const status = { state: 2, description: 'connected' }; + const status = { state: App.StatusEnum.CONNECTED, description: 'connected' }; expect(Actions.updateStatus(status)).toEqual({ type: Types.UPDATE_STATUS, status }); }); @@ -116,7 +110,7 @@ describe('Actions', () => { }); it('viewLogs', () => { - const logs = [{ targetType: 'room' }] as any[]; + const logs = [create(Data.ServerInfo_ChatMessageSchema, { targetType: 'room' })]; expect(Actions.viewLogs(logs)).toEqual({ type: Types.VIEW_LOGS, logs }); }); @@ -153,7 +147,7 @@ describe('Actions', () => { }); it('accountAwaitingActivation', () => { - const options = makeConnectOptions(); + const options = makePendingActivationContext(); expect(Actions.accountAwaitingActivation(options)).toEqual({ type: Types.ACCOUNT_AWAITING_ACTIVATION, options }); }); @@ -222,17 +216,17 @@ describe('Actions', () => { }); it('notifyUser', () => { - const notification = create(Event_NotifyUserSchema, { type: 1, warningReason: '', customTitle: '', customContent: '' }); + const notification = create(Data.Event_NotifyUserSchema, { type: 1, warningReason: '', customTitle: '', customContent: '' }); expect(Actions.notifyUser(notification)).toEqual({ type: Types.NOTIFY_USER, notification }); }); it('serverShutdown', () => { - const data = create(Event_ServerShutdownSchema, { reason: 'maintenance', minutes: 5 }); + const data = create(Data.Event_ServerShutdownSchema, { reason: 'maintenance', minutes: 5 }); expect(Actions.serverShutdown(data)).toEqual({ type: Types.SERVER_SHUTDOWN, data }); }); it('userMessage', () => { - const messageData = create(Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'hey' }); + const messageData = create(Data.Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'hey' }); expect(Actions.userMessage(messageData)).toEqual({ type: Types.USER_MESSAGE, messageData }); }); @@ -360,9 +354,8 @@ describe('Actions', () => { }); it('gamesOfUser', () => { - const games = [makeGame({ gameId: 1 })]; - const gametypeMap = { 1: 'Standard' }; - expect(Actions.gamesOfUser('alice', games, gametypeMap)).toEqual({ type: Types.GAMES_OF_USER, userName: 'alice', games, gametypeMap }); + const response = create(Data.Response_GetGamesOfUserSchema, { roomList: [], gameList: [] }); + expect(Actions.gamesOfUser('alice', response)).toEqual({ type: Types.GAMES_OF_USER, userName: 'alice', response }); }); it('clearRegistrationErrors', () => { diff --git a/webclient/src/store/server/server.actions.ts b/webclient/src/store/server/server.actions.ts index a29f4d49d..143a800f7 100644 --- a/webclient/src/store/server/server.actions.ts +++ b/webclient/src/store/server/server.actions.ts @@ -1,16 +1,4 @@ -import { - GametypeMap, WebSocketConnectOptions -} from 'types'; -import type { ServerInfo_User } from 'generated/proto/serverinfo_user_pb'; -import type { ServerInfo_Ban } from 'generated/proto/serverinfo_ban_pb'; -import type { ServerInfo_Warning } from 'generated/proto/serverinfo_warning_pb'; -import type { Response_WarnList } from 'generated/proto/response_warn_list_pb'; -import type { ServerInfo_ReplayMatch } from 'generated/proto/serverinfo_replay_match_pb'; -import type { Response_DeckList } from 'generated/proto/response_deck_list_pb'; -import type { ServerInfo_DeckStorage_TreeItem } from 'generated/proto/serverinfo_deckstorage_pb'; -import type { ServerInfo_ChatMessage } from 'generated/proto/serverinfo_chat_message_pb'; -import type { ServerInfo_Game } from 'generated/proto/serverinfo_game_pb'; -import { NotifyUserData, ServerShutdownData, UserMessageData } from 'websocket/events/session/interfaces'; +import { Data, Enriched } from '@app/types'; import { ServerStateStatus } from './server.interfaces'; import { Types } from './server.types'; @@ -24,17 +12,13 @@ export const Actions = { connectionAttempted: () => ({ type: Types.CONNECTION_ATTEMPTED }), - loginSuccessful: (options: WebSocketConnectOptions) => ({ + loginSuccessful: (options: Enriched.LoginSuccessContext) => ({ type: Types.LOGIN_SUCCESSFUL, options }), loginFailed: () => ({ type: Types.LOGIN_FAILED, }), - connectionClosed: (reason: number) => ({ - type: Types.CONNECTION_CLOSED, - reason - }), connectionFailed: () => ({ type: Types.CONNECTION_FAILED, }), @@ -48,11 +32,11 @@ export const Actions = { type: Types.SERVER_MESSAGE, message }), - updateBuddyList: (buddyList: ServerInfo_User[]) => ({ + updateBuddyList: (buddyList: Data.ServerInfo_User[]) => ({ type: Types.UPDATE_BUDDY_LIST, buddyList }), - addToBuddyList: (user: ServerInfo_User) => ({ + addToBuddyList: (user: Data.ServerInfo_User) => ({ type: Types.ADD_TO_BUDDY_LIST, user }), @@ -60,11 +44,11 @@ export const Actions = { type: Types.REMOVE_FROM_BUDDY_LIST, userName }), - updateIgnoreList: (ignoreList: ServerInfo_User[]) => ({ + updateIgnoreList: (ignoreList: Data.ServerInfo_User[]) => ({ type: Types.UPDATE_IGNORE_LIST, ignoreList }), - addToIgnoreList: (user: ServerInfo_User) => ({ + addToIgnoreList: (user: Data.ServerInfo_User) => ({ type: Types.ADD_TO_IGNORE_LIST, user }), @@ -76,19 +60,19 @@ export const Actions = { type: Types.UPDATE_INFO, info }), - updateStatus: (status: ServerStateStatus) => ({ + updateStatus: (status: Pick) => ({ type: Types.UPDATE_STATUS, status }), - updateUser: (user: ServerInfo_User) => ({ + updateUser: (user: Data.ServerInfo_User) => ({ type: Types.UPDATE_USER, user }), - updateUsers: (users: ServerInfo_User[]) => ({ + updateUsers: (users: Data.ServerInfo_User[]) => ({ type: Types.UPDATE_USERS, users }), - userJoined: (user: ServerInfo_User) => ({ + userJoined: (user: Data.ServerInfo_User) => ({ type: Types.USER_JOINED, user }), @@ -96,7 +80,7 @@ export const Actions = { type: Types.USER_LEFT, name }), - viewLogs: (logs: ServerInfo_ChatMessage[]) => ({ + viewLogs: (logs: Data.ServerInfo_ChatMessage[]) => ({ type: Types.VIEW_LOGS, logs }), @@ -129,7 +113,7 @@ export const Actions = { clearRegistrationErrors: () => ({ type: Types.CLEAR_REGISTRATION_ERRORS, }), - accountAwaitingActivation: (options: WebSocketConnectOptions) => ({ + accountAwaitingActivation: (options: Enriched.PendingActivationContext) => ({ type: Types.ACCOUNT_AWAITING_ACTIVATION, options }), @@ -169,27 +153,27 @@ export const Actions = { accountPasswordChange: () => ({ type: Types.ACCOUNT_PASSWORD_CHANGE, }), - accountEditChanged: (user: Partial) => ({ + accountEditChanged: (user: Partial) => ({ type: Types.ACCOUNT_EDIT_CHANGED, user, }), - accountImageChanged: (user: Partial) => ({ + accountImageChanged: (user: Partial) => ({ type: Types.ACCOUNT_IMAGE_CHANGED, user, }), - getUserInfo: (userInfo: ServerInfo_User) => ({ + getUserInfo: (userInfo: Data.ServerInfo_User) => ({ type: Types.GET_USER_INFO, userInfo, }), - notifyUser: (notification: NotifyUserData) => ({ + notifyUser: (notification: Data.Event_NotifyUser) => ({ type: Types.NOTIFY_USER, notification, }), - serverShutdown: (data: ServerShutdownData) => ({ + serverShutdown: (data: Data.Event_ServerShutdown) => ({ type: Types.SERVER_SHUTDOWN, data, }), - userMessage: (messageData: UserMessageData) => ({ + userMessage: (messageData: Data.Event_UserMessage) => ({ type: Types.USER_MESSAGE, messageData, }), @@ -207,17 +191,17 @@ export const Actions = { type: Types.BAN_FROM_SERVER, userName, }), - banHistory: (userName: string, banHistory: ServerInfo_Ban[]) => ({ + banHistory: (userName: string, banHistory: Data.ServerInfo_Ban[]) => ({ type: Types.BAN_HISTORY, userName, banHistory, }), - warnHistory: (userName: string, warnHistory: ServerInfo_Warning[]) => ({ + warnHistory: (userName: string, warnHistory: Data.ServerInfo_Warning[]) => ({ type: Types.WARN_HISTORY, userName, warnHistory, }), - warnListOptions: (warnList: Response_WarnList[]) => ({ + warnListOptions: (warnList: Data.Response_WarnList[]) => ({ type: Types.WARN_LIST_OPTIONS, warnList, }), @@ -245,17 +229,17 @@ export const Actions = { userName, notes, }), - replayList: (matchList: ServerInfo_ReplayMatch[]) => ({ type: Types.REPLAY_LIST, matchList }), - replayAdded: (matchInfo: ServerInfo_ReplayMatch) => ({ type: Types.REPLAY_ADDED, matchInfo }), + replayList: (matchList: Data.ServerInfo_ReplayMatch[]) => ({ type: Types.REPLAY_LIST, matchList }), + replayAdded: (matchInfo: Data.ServerInfo_ReplayMatch) => ({ type: Types.REPLAY_ADDED, matchInfo }), replayModifyMatch: (gameId: number, doNotHide: boolean) => ({ type: Types.REPLAY_MODIFY_MATCH, gameId, doNotHide }), replayDeleteMatch: (gameId: number) => ({ type: Types.REPLAY_DELETE_MATCH, gameId }), - backendDecks: (deckList: Response_DeckList) => ({ type: Types.BACKEND_DECKS, deckList }), + backendDecks: (deckList: Data.Response_DeckList) => ({ type: Types.BACKEND_DECKS, deckList }), deckNewDir: (path: string, dirName: string) => ({ type: Types.DECK_NEW_DIR, path, dirName }), deckDelDir: (path: string) => ({ type: Types.DECK_DEL_DIR, path }), - deckUpload: (path: string, treeItem: ServerInfo_DeckStorage_TreeItem) => ({ type: Types.DECK_UPLOAD, path, treeItem }), + deckUpload: (path: string, treeItem: Data.ServerInfo_DeckStorage_TreeItem) => ({ type: Types.DECK_UPLOAD, path, treeItem }), deckDelete: (deckId: number) => ({ type: Types.DECK_DELETE, deckId }), - gamesOfUser: (userName: string, games: ServerInfo_Game[], gametypeMap: GametypeMap) => - ({ type: Types.GAMES_OF_USER, userName, games, gametypeMap }), + gamesOfUser: (userName: string, response: Data.Response_GetGamesOfUser) => + ({ type: Types.GAMES_OF_USER, userName, response }), } export type ServerAction = ReturnType; diff --git a/webclient/src/store/server/server.dispatch.spec.ts b/webclient/src/store/server/server.dispatch.spec.ts index ad2365a79..d58d3fcb1 100644 --- a/webclient/src/store/server/server.dispatch.spec.ts +++ b/webclient/src/store/server/server.dispatch.spec.ts @@ -1,26 +1,22 @@ -vi.mock('store', () => ({ store: { dispatch: vi.fn() } })); +vi.mock('..', () => ({ store: { dispatch: vi.fn() } })); -import { store } from 'store'; +import { store } from '..'; import { Actions } from './server.actions'; import { Dispatch } from './server.dispatch'; +import { App, Data } from '@app/types'; import { create } from '@bufbuild/protobuf'; -import { Event_NotifyUserSchema } from 'generated/proto/event_notify_user_pb'; -import { Event_ServerShutdownSchema } from 'generated/proto/event_server_shutdown_pb'; -import { Event_UserMessageSchema } from 'generated/proto/event_user_message_pb'; import { makeBanHistoryItem, - makeConnectOptions, + makeLoginSuccessContext, + makePendingActivationContext, makeDeckList, makeDeckTreeItem, - makeGame, makeReplayMatch, makeUser, makeWarnHistoryItem, makeWarnListItem, } from './__mocks__/server-fixtures'; -beforeEach(() => vi.clearAllMocks()); - describe('Dispatch', () => { it('initialized dispatches Actions.initialized()', () => { Dispatch.initialized(); @@ -38,7 +34,7 @@ describe('Dispatch', () => { }); it('loginSuccessful dispatches Actions.loginSuccessful()', () => { - const options = makeConnectOptions(); + const options = makeLoginSuccessContext(); Dispatch.loginSuccessful(options); expect(store.dispatch).toHaveBeenCalledWith(Actions.loginSuccessful(options)); }); @@ -48,11 +44,6 @@ describe('Dispatch', () => { expect(store.dispatch).toHaveBeenCalledWith(Actions.loginFailed()); }); - it('connectionClosed dispatches Actions.connectionClosed()', () => { - Dispatch.connectionClosed(3); - expect(store.dispatch).toHaveBeenCalledWith(Actions.connectionClosed(3)); - }); - it('connectionFailed dispatches Actions.connectionFailed()', () => { Dispatch.connectionFailed(); expect(store.dispatch).toHaveBeenCalledWith(Actions.connectionFailed()); @@ -108,8 +99,8 @@ describe('Dispatch', () => { }); it('updateStatus dispatches Actions.updateStatus({ state, description })', () => { - Dispatch.updateStatus(2, 'ok'); - expect(store.dispatch).toHaveBeenCalledWith(Actions.updateStatus({ state: 2, description: 'ok' })); + Dispatch.updateStatus(App.StatusEnum.CONNECTED, 'ok'); + expect(store.dispatch).toHaveBeenCalledWith(Actions.updateStatus({ state: App.StatusEnum.CONNECTED, description: 'ok' })); }); it('updateUser dispatches Actions.updateUser()', () => { @@ -136,7 +127,7 @@ describe('Dispatch', () => { }); it('viewLogs dispatches Actions.viewLogs()', () => { - const logs = [{ targetType: 'room' }] as any[]; + const logs = [create(Data.ServerInfo_ChatMessageSchema, { targetType: 'room' })]; Dispatch.viewLogs(logs); expect(store.dispatch).toHaveBeenCalledWith(Actions.viewLogs(logs)); }); @@ -187,7 +178,7 @@ describe('Dispatch', () => { }); it('accountAwaitingActivation dispatches correctly', () => { - const options = makeConnectOptions(); + const options = makePendingActivationContext(); Dispatch.accountAwaitingActivation(options); expect(store.dispatch).toHaveBeenCalledWith(Actions.accountAwaitingActivation(options)); }); @@ -266,19 +257,19 @@ describe('Dispatch', () => { }); it('notifyUser dispatches correctly', () => { - const notification = create(Event_NotifyUserSchema, { type: 1, warningReason: '', customTitle: '', customContent: '' }); + const notification = create(Data.Event_NotifyUserSchema, { type: 1, warningReason: '', customTitle: '', customContent: '' }); Dispatch.notifyUser(notification); expect(store.dispatch).toHaveBeenCalledWith(Actions.notifyUser(notification)); }); it('serverShutdown dispatches correctly', () => { - const data = create(Event_ServerShutdownSchema, { reason: 'maintenance', minutes: 5 }); + const data = create(Data.Event_ServerShutdownSchema, { reason: 'maintenance', minutes: 5 }); Dispatch.serverShutdown(data); expect(store.dispatch).toHaveBeenCalledWith(Actions.serverShutdown(data)); }); it('userMessage dispatches correctly', () => { - const messageData = create(Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'hey' }); + const messageData = create(Data.Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'hey' }); Dispatch.userMessage(messageData); expect(store.dispatch).toHaveBeenCalledWith(Actions.userMessage(messageData)); }); @@ -391,10 +382,9 @@ describe('Dispatch', () => { }); it('gamesOfUser dispatches correctly', () => { - const games = [makeGame({ gameId: 1 })]; - const gametypeMap = { 1: 'Standard' }; - Dispatch.gamesOfUser('alice', games, gametypeMap); - expect(store.dispatch).toHaveBeenCalledWith(Actions.gamesOfUser('alice', games, gametypeMap)); + const response = create(Data.Response_GetGamesOfUserSchema, { roomList: [], gameList: [] }); + Dispatch.gamesOfUser('alice', response); + expect(store.dispatch).toHaveBeenCalledWith(Actions.gamesOfUser('alice', response)); }); it('clearRegistrationErrors dispatches correctly', () => { diff --git a/webclient/src/store/server/server.dispatch.ts b/webclient/src/store/server/server.dispatch.ts index 19db7ced1..1438d96d7 100644 --- a/webclient/src/store/server/server.dispatch.ts +++ b/webclient/src/store/server/server.dispatch.ts @@ -1,18 +1,6 @@ import { Actions } from './server.actions'; -import { store } from 'store'; -import { - GametypeMap, WebSocketConnectOptions -} from 'types'; -import type { ServerInfo_User } from 'generated/proto/serverinfo_user_pb'; -import type { ServerInfo_Ban } from 'generated/proto/serverinfo_ban_pb'; -import type { ServerInfo_Warning } from 'generated/proto/serverinfo_warning_pb'; -import type { Response_WarnList } from 'generated/proto/response_warn_list_pb'; -import type { ServerInfo_ReplayMatch } from 'generated/proto/serverinfo_replay_match_pb'; -import type { Response_DeckList } from 'generated/proto/response_deck_list_pb'; -import type { ServerInfo_DeckStorage_TreeItem } from 'generated/proto/serverinfo_deckstorage_pb'; -import type { ServerInfo_ChatMessage } from 'generated/proto/serverinfo_chat_message_pb'; -import type { ServerInfo_Game } from 'generated/proto/serverinfo_game_pb'; -import { NotifyUserData, ServerShutdownData, UserMessageData } from 'websocket/events/session/interfaces'; +import { store } from '..'; +import { App, Data, Enriched } from '@app/types'; export const Dispatch = { initialized: () => { @@ -24,15 +12,12 @@ export const Dispatch = { connectionAttempted: () => { store.dispatch(Actions.connectionAttempted()); }, - loginSuccessful: (options: WebSocketConnectOptions) => { + loginSuccessful: (options: Enriched.LoginSuccessContext) => { store.dispatch(Actions.loginSuccessful(options)); }, loginFailed: () => { store.dispatch(Actions.loginFailed()); }, - connectionClosed: (reason: number) => { - store.dispatch(Actions.connectionClosed(reason)); - }, connectionFailed: () => { store.dispatch(Actions.connectionFailed()); }, @@ -42,19 +27,19 @@ export const Dispatch = { testConnectionFailed: () => { store.dispatch(Actions.testConnectionFailed()); }, - updateBuddyList: (buddyList: ServerInfo_User[]) => { + updateBuddyList: (buddyList: Data.ServerInfo_User[]) => { store.dispatch(Actions.updateBuddyList(buddyList)); }, - addToBuddyList: (user: ServerInfo_User) => { + addToBuddyList: (user: Data.ServerInfo_User) => { store.dispatch(Actions.addToBuddyList(user)); }, removeFromBuddyList: (userName: string) => { store.dispatch(Actions.removeFromBuddyList(userName)); }, - updateIgnoreList: (ignoreList: ServerInfo_User[]) => { + updateIgnoreList: (ignoreList: Data.ServerInfo_User[]) => { store.dispatch(Actions.updateIgnoreList(ignoreList)); }, - addToIgnoreList: (user: ServerInfo_User) => { + addToIgnoreList: (user: Data.ServerInfo_User) => { store.dispatch(Actions.addToIgnoreList(user)); }, removeFromIgnoreList: (userName: string) => { @@ -66,25 +51,25 @@ export const Dispatch = { version })); }, - updateStatus: (state: number, description: string) => { + updateStatus: (state: App.StatusEnum, description: string) => { store.dispatch(Actions.updateStatus({ state, description })); }, - updateUser: (user: ServerInfo_User) => { + updateUser: (user: Data.ServerInfo_User) => { store.dispatch(Actions.updateUser(user)); }, - updateUsers: (users: ServerInfo_User[]) => { + updateUsers: (users: Data.ServerInfo_User[]) => { store.dispatch(Actions.updateUsers(users)); }, - userJoined: (user: ServerInfo_User) => { + userJoined: (user: Data.ServerInfo_User) => { store.dispatch(Actions.userJoined(user)); }, userLeft: (name: string) => { store.dispatch(Actions.userLeft(name)); }, - viewLogs: (logs: ServerInfo_ChatMessage[]) => { + viewLogs: (logs: Data.ServerInfo_ChatMessage[]) => { store.dispatch(Actions.viewLogs(logs)); }, clearLogs: () => { @@ -114,7 +99,7 @@ export const Dispatch = { registrationUserNameError: (error: string) => { store.dispatch(Actions.registrationUserNameError(error)); }, - accountAwaitingActivation: (options: WebSocketConnectOptions) => { + accountAwaitingActivation: (options: Enriched.PendingActivationContext) => { store.dispatch(Actions.accountAwaitingActivation(options)); }, accountActivationSuccess: () => { @@ -150,22 +135,22 @@ export const Dispatch = { accountPasswordChange: () => { store.dispatch(Actions.accountPasswordChange()); }, - accountEditChanged: (user: Partial) => { + accountEditChanged: (user: Partial) => { store.dispatch(Actions.accountEditChanged(user)); }, - accountImageChanged: (user: Partial) => { + accountImageChanged: (user: Partial) => { store.dispatch(Actions.accountImageChanged(user)); }, - getUserInfo: (userInfo: ServerInfo_User) => { + getUserInfo: (userInfo: Data.ServerInfo_User) => { store.dispatch(Actions.getUserInfo(userInfo)); }, - notifyUser: (notification: NotifyUserData) => { + notifyUser: (notification: Data.Event_NotifyUser) => { store.dispatch(Actions.notifyUser(notification)) }, - serverShutdown: (data: ServerShutdownData) => { + serverShutdown: (data: Data.Event_ServerShutdown) => { store.dispatch(Actions.serverShutdown(data)) }, - userMessage: (messageData: UserMessageData) => { + userMessage: (messageData: Data.Event_UserMessage) => { store.dispatch(Actions.userMessage(messageData)) }, addToList: (list: string, userName: string) => { @@ -177,13 +162,13 @@ export const Dispatch = { banFromServer: (userName: string) => { store.dispatch(Actions.banFromServer(userName)); }, - banHistory: (userName: string, banHistory: ServerInfo_Ban[]) => { + banHistory: (userName: string, banHistory: Data.ServerInfo_Ban[]) => { store.dispatch(Actions.banHistory(userName, banHistory)) }, - warnHistory: (userName: string, warnHistory: ServerInfo_Warning[]) => { + warnHistory: (userName: string, warnHistory: Data.ServerInfo_Warning[]) => { store.dispatch(Actions.warnHistory(userName, warnHistory)) }, - warnListOptions: (warnList: Response_WarnList[]) => { + warnListOptions: (warnList: Data.Response_WarnList[]) => { store.dispatch(Actions.warnListOptions(warnList)) }, warnUser: (userName: string) => { @@ -201,10 +186,10 @@ export const Dispatch = { updateAdminNotes: (userName: string, notes: string) => { store.dispatch(Actions.updateAdminNotes(userName, notes)); }, - replayList: (matchList: ServerInfo_ReplayMatch[]) => { + replayList: (matchList: Data.ServerInfo_ReplayMatch[]) => { store.dispatch(Actions.replayList(matchList)); }, - replayAdded: (matchInfo: ServerInfo_ReplayMatch) => { + replayAdded: (matchInfo: Data.ServerInfo_ReplayMatch) => { store.dispatch(Actions.replayAdded(matchInfo)); }, replayModifyMatch: (gameId: number, doNotHide: boolean) => { @@ -213,7 +198,7 @@ export const Dispatch = { replayDeleteMatch: (gameId: number) => { store.dispatch(Actions.replayDeleteMatch(gameId)); }, - backendDecks: (deckList: Response_DeckList) => { + backendDecks: (deckList: Data.Response_DeckList) => { store.dispatch(Actions.backendDecks(deckList)); }, deckNewDir: (path: string, dirName: string) => { @@ -222,13 +207,13 @@ export const Dispatch = { deckDelDir: (path: string) => { store.dispatch(Actions.deckDelDir(path)); }, - deckUpload: (path: string, treeItem: ServerInfo_DeckStorage_TreeItem) => { + deckUpload: (path: string, treeItem: Data.ServerInfo_DeckStorage_TreeItem) => { store.dispatch(Actions.deckUpload(path, treeItem)); }, deckDelete: (deckId: number) => { store.dispatch(Actions.deckDelete(deckId)); }, - gamesOfUser: (userName: string, games: ServerInfo_Game[], gametypeMap: GametypeMap) => { - store.dispatch(Actions.gamesOfUser(userName, games, gametypeMap)); + gamesOfUser: (userName: string, response: Data.Response_GetGamesOfUser) => { + store.dispatch(Actions.gamesOfUser(userName, response)); }, } diff --git a/webclient/src/store/server/server.interfaces.ts b/webclient/src/store/server/server.interfaces.ts index 3791c3509..371a67654 100644 --- a/webclient/src/store/server/server.interfaces.ts +++ b/webclient/src/store/server/server.interfaces.ts @@ -1,105 +1,57 @@ -import { - Game, SortBy, UserSortField -} from 'types'; -import type { ServerInfo_User } from 'generated/proto/serverinfo_user_pb'; -import type { ServerInfo_Ban } from 'generated/proto/serverinfo_ban_pb'; -import type { ServerInfo_Warning } from 'generated/proto/serverinfo_warning_pb'; -import type { Response_WarnList } from 'generated/proto/response_warn_list_pb'; -import type { ServerInfo_ReplayMatch } from 'generated/proto/serverinfo_replay_match_pb'; -import type { Response_DeckList } from 'generated/proto/response_deck_list_pb'; -import type { ServerInfo_ChatMessage } from 'generated/proto/serverinfo_chat_message_pb'; -import { NotifyUserData, ServerShutdownData, UserMessageData } from 'websocket/events/session/interfaces'; - -export interface ServerConnectParams { - host: string; - port: string; - userName: string; - password: string; -} - -export interface ServerRegisterParams { - host: string; - port: string; - userName: string; - password: string; - email: string; - country: string; - realName: string; -} - -export interface RequestPasswordSaltParams { - userName: string; -} - -export interface ForgotPasswordParams { - userName: string; -} - -export interface ForgotPasswordChallengeParams extends ForgotPasswordParams { - email: string; -} - -export interface ForgotPasswordResetParams extends ForgotPasswordParams { - token: string; - newPassword: string; -} - -export interface AccountActivationParams extends ServerRegisterParams { - token: string; -} +import { App, Data, Enriched } from '@app/types'; export interface ServerState { initialized: boolean; - buddyList: ServerInfo_User[]; - ignoreList: ServerInfo_User[]; + buddyList: Data.ServerInfo_User[]; + ignoreList: Data.ServerInfo_User[]; info: ServerStateInfo; status: ServerStateStatus; logs: ServerStateLogs; - user: ServerInfo_User; - users: ServerInfo_User[]; + user: Data.ServerInfo_User | null; + users: Data.ServerInfo_User[]; sortUsersBy: ServerStateSortUsersBy; messages: { - [userName: string]: UserMessageData[]; + [userName: string]: Data.Event_UserMessage[]; } userInfo: { - [userName: string]: ServerInfo_User; + [userName: string]: Data.ServerInfo_User; } - notifications: NotifyUserData[]; - serverShutdown: ServerShutdownData; + notifications: Data.Event_NotifyUser[]; + serverShutdown: Data.Event_ServerShutdown | null; banUser: string; banHistory: { - [userName: string]: ServerInfo_Ban[]; + [userName: string]: Data.ServerInfo_Ban[]; }; warnHistory: { - [userName: string]: ServerInfo_Warning[]; + [userName: string]: Data.ServerInfo_Warning[]; }; - warnListOptions: Response_WarnList[]; + warnListOptions: Data.Response_WarnList[]; warnUser: string; adminNotes: { [userName: string]: string }; - replays: ServerInfo_ReplayMatch[]; - backendDecks: Response_DeckList | null; - gamesOfUser: { [userName: string]: Game[] }; + replays: Data.ServerInfo_ReplayMatch[]; + backendDecks: Data.Response_DeckList | null; + gamesOfUser: { [userName: string]: Enriched.Game[] }; registrationError: string | null; } export interface ServerStateStatus { connectionAttemptMade: boolean; - description: string; - state: number; + description: string | null; + state: App.StatusEnum; } export interface ServerStateInfo { - message: string; - name: string; - version: string; + message: string | null; + name: string | null; + version: string | null; } export interface ServerStateLogs { - room: ServerInfo_ChatMessage[]; - game: ServerInfo_ChatMessage[]; - chat: ServerInfo_ChatMessage[]; + room: Data.ServerInfo_ChatMessage[]; + game: Data.ServerInfo_ChatMessage[]; + chat: Data.ServerInfo_ChatMessage[]; } -export interface ServerStateSortUsersBy extends SortBy { - field: UserSortField +export interface ServerStateSortUsersBy extends App.SortBy { + field: App.UserSortField } diff --git a/webclient/src/store/server/server.reducer.spec.ts b/webclient/src/store/server/server.reducer.spec.ts index 573683a76..bee69ac50 100644 --- a/webclient/src/store/server/server.reducer.spec.ts +++ b/webclient/src/store/server/server.reducer.spec.ts @@ -1,13 +1,10 @@ -import { StatusEnum } from 'types'; -import { ServerInfo_User_UserLevelFlag as UserLevelFlag } from 'generated/proto/serverinfo_user_pb'; +import { App, Data } from '@app/types'; import { create } from '@bufbuild/protobuf'; -import { Event_UserMessageSchema } from 'generated/proto/event_user_message_pb'; -import { ServerInfo_DeckStorage_FolderSchema, ServerInfo_DeckStorage_TreeItemSchema } from 'generated/proto/serverinfo_deckstorage_pb'; import { serverReducer } from './server.reducer'; import { Types } from './server.types'; import { makeBanHistoryItem, - makeConnectOptions, + makePendingActivationContext, makeDeckList, makeDeckTreeItem, makeGame, @@ -19,6 +16,8 @@ import { makeWarnListItem, } from './__mocks__/server-fixtures'; +const UserLevelFlag = Data.ServerInfo_User_UserLevelFlag; + // ── Initialisation ─────────────────────────────────────────────────────────── describe('Initialisation', () => { @@ -26,7 +25,7 @@ describe('Initialisation', () => { const result = serverReducer(undefined, { type: '@@INIT' }); expect(result.initialized).toBe(false); expect(result.buddyList).toEqual([]); - expect(result.status.state).toBe(StatusEnum.DISCONNECTED); + expect(result.status.state).toBe(App.StatusEnum.DISCONNECTED); }); it('INITIALIZED → resets to initialState with initialized: true', () => { @@ -38,7 +37,7 @@ describe('Initialisation', () => { }); it('CLEAR_STORE → resets to initialState but preserves status', () => { - const status = { state: StatusEnum.LOGGED_IN, description: 'logged in' }; + const status = { state: App.StatusEnum.LOGGED_IN, description: 'logged in', connectionAttemptMade: true }; const state = makeServerState({ status, banUser: 'someone' }); const result = serverReducer(state, { type: Types.CLEAR_STORE }); expect(result.banUser).toBe(''); @@ -57,13 +56,13 @@ describe('Initialisation', () => { describe('Account & Connection', () => { it('CONNECTION_ATTEMPTED → sets connectionAttemptMade to true', () => { - const state = makeServerState({ status: { connectionAttemptMade: false, state: StatusEnum.DISCONNECTED, description: null } }); + const state = makeServerState({ status: { connectionAttemptMade: false, state: App.StatusEnum.DISCONNECTED, description: null } }); const result = serverReducer(state, { type: Types.CONNECTION_ATTEMPTED }); expect(result.status.connectionAttemptMade).toBe(true); }); it('ACCOUNT_AWAITING_ACTIVATION → returns state unchanged', () => { - const options = makeConnectOptions(); + const options = makePendingActivationContext(); const state = makeServerState(); const result = serverReducer(state, { type: Types.ACCOUNT_AWAITING_ACTIVATION, options }); expect(result).toBe(state); @@ -133,11 +132,13 @@ describe('Server Info & Status', () => { expect(result.info.message).toBe('hi'); }); - it('UPDATE_STATUS → replaces state.status entirely', () => { + it('UPDATE_STATUS → merges state and description into status', () => { const state = makeServerState(); - const status = { state: StatusEnum.LOGGED_IN, description: 'ok' }; - const result = serverReducer(state, { type: Types.UPDATE_STATUS, status }); - expect(result.status).toEqual(status); + const update = { state: App.StatusEnum.LOGGED_IN, description: 'ok' }; + const result = serverReducer(state, { type: Types.UPDATE_STATUS, status: update }); + expect(result.status.state).toBe(App.StatusEnum.LOGGED_IN); + expect(result.status.description).toBe('ok'); + expect(result.status.connectionAttemptMade).toBe(false); }); }); @@ -281,12 +282,12 @@ describe('Messaging', () => { }); it('USER_MESSAGE → appends to existing messages for that user', () => { - const existingMsg = create(Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'first' }); + const existingMsg = create(Data.Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'first' }); const state = makeServerState({ user: makeUser({ name: 'Bob' }), messages: { Alice: [existingMsg] }, }); - const newMsg = create(Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'second' }); + const newMsg = create(Data.Event_UserMessageSchema, { senderName: 'Alice', receiverName: 'Bob', message: 'second' }); const result = serverReducer(state, { type: Types.USER_MESSAGE, messageData: newMsg }); expect(result.messages['Alice']).toHaveLength(2); }); @@ -482,11 +483,11 @@ describe('Deck Storage', () => { }); it('DECK_UPLOAD with nested path → inserts into matching subfolder', () => { - const subfolder = create(ServerInfo_DeckStorage_TreeItemSchema, { - id: 0, name: 'myDecks', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] }) + const subfolder = create(Data.ServerInfo_DeckStorage_TreeItemSchema, { + id: 0, name: 'myDecks', folder: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [] }) }); const state = makeServerState({ - backendDecks: makeDeckList({ root: create(ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) }) + backendDecks: makeDeckList({ root: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) }) }); const item = makeDeckTreeItem({ name: 'new.cod' }); const result = serverReducer(state, { type: Types.DECK_UPLOAD, path: 'myDecks', treeItem: item }); @@ -512,18 +513,20 @@ describe('Deck Storage', () => { it('DECK_DELETE → removes item by id from tree', () => { const item = makeDeckTreeItem({ id: 7 }); - const state = makeServerState({ backendDecks: makeDeckList({ root: create(ServerInfo_DeckStorage_FolderSchema, { items: [item] }) }) }); + const state = makeServerState({ + backendDecks: makeDeckList({ root: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [item] }) }), + }); const result = serverReducer(state, { type: Types.DECK_DELETE, deckId: 7 }); expect(result.backendDecks.root.items).toHaveLength(0); }); it('DECK_DELETE → recursively removes item nested inside a subfolder', () => { const nested = makeDeckTreeItem({ id: 9, name: 'nested.cod' }); - const subfolder = create(ServerInfo_DeckStorage_TreeItemSchema, { - id: 0, name: 'sub', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [nested] }) + const subfolder = create(Data.ServerInfo_DeckStorage_TreeItemSchema, { + id: 0, name: 'sub', folder: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [nested] }) }); const state = makeServerState({ - backendDecks: makeDeckList({ root: create(ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) }) + backendDecks: makeDeckList({ root: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) }) }); const result = serverReducer(state, { type: Types.DECK_DELETE, deckId: 9 }); expect(result.backendDecks.root.items[0].folder.items).toHaveLength(0); @@ -544,11 +547,11 @@ describe('Deck Storage', () => { }); it('DECK_NEW_DIR nested → inserts folder inside matching subfolder', () => { - const subfolder = create(ServerInfo_DeckStorage_TreeItemSchema, { - id: 0, name: 'parent', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] }) + const subfolder = create(Data.ServerInfo_DeckStorage_TreeItemSchema, { + id: 0, name: 'parent', folder: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [] }) }); const state = makeServerState({ - backendDecks: makeDeckList({ root: create(ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) }) + backendDecks: makeDeckList({ root: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) }) }); const result = serverReducer(state, { type: Types.DECK_NEW_DIR, path: 'parent', dirName: 'child' }); const parent = result.backendDecks.root.items.find(i => i.name === 'parent'); @@ -563,36 +566,36 @@ describe('Deck Storage', () => { }); it('DECK_DEL_DIR → removes folder from root by name', () => { - const subfolder = create(ServerInfo_DeckStorage_TreeItemSchema, { - id: 0, name: 'myDir', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] }) + const subfolder = create(Data.ServerInfo_DeckStorage_TreeItemSchema, { + id: 0, name: 'myDir', folder: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [] }) }); const state = makeServerState({ - backendDecks: makeDeckList({ root: create(ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) }) + backendDecks: makeDeckList({ root: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) }) }); const result = serverReducer(state, { type: Types.DECK_DEL_DIR, path: 'myDir' }); expect(result.backendDecks.root.items).toHaveLength(0); }); it('DECK_DEL_DIR → returns deck tree unchanged when path is empty', () => { - const subfolder = create(ServerInfo_DeckStorage_TreeItemSchema, { - id: 0, name: 'keep', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] }) + const subfolder = create(Data.ServerInfo_DeckStorage_TreeItemSchema, { + id: 0, name: 'keep', folder: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [] }) }); const state = makeServerState({ - backendDecks: makeDeckList({ root: create(ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) }) + backendDecks: makeDeckList({ root: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [subfolder] }) }) }); const result = serverReducer(state, { type: Types.DECK_DEL_DIR, path: '' }); expect(result.backendDecks.root.items).toHaveLength(1); }); it('DECK_DEL_DIR → recursively removes nested subfolder via multi-segment path', () => { - const child = create(ServerInfo_DeckStorage_TreeItemSchema, { - id: 0, name: 'child', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] }) + const child = create(Data.ServerInfo_DeckStorage_TreeItemSchema, { + id: 0, name: 'child', folder: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [] }) }); - const parent = create(ServerInfo_DeckStorage_TreeItemSchema, { - id: 0, name: 'parent', folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [child] }) + const parent = create(Data.ServerInfo_DeckStorage_TreeItemSchema, { + id: 0, name: 'parent', folder: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [child] }) }); const state = makeServerState({ - backendDecks: makeDeckList({ root: create(ServerInfo_DeckStorage_FolderSchema, { items: [parent] }) }) + backendDecks: makeDeckList({ root: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [parent] }) }) }); const result = serverReducer(state, { type: Types.DECK_DEL_DIR, path: 'parent/child' }); expect(result.backendDecks.root.items[0].folder.items).toHaveLength(0); @@ -603,24 +606,31 @@ describe('Deck Storage', () => { describe('GAMES_OF_USER', () => { it('stores normalized games keyed by userName', () => { - const games = [makeGame({ gameId: 5 })]; + const response = create(Data.Response_GetGamesOfUserSchema, { + gameList: [create(Data.ServerInfo_GameSchema, { gameId: 5, description: '' })], + roomList: [], + }); const state = makeServerState(); - const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games, gametypeMap: {} }); - expect(result.gamesOfUser['alice']).toEqual(games); + const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', response }); + expect(result.gamesOfUser['alice']).toEqual([makeGame({ gameId: 5 })]); }); it('overwrites previous games for same user', () => { const old = [makeGame({ gameId: 1 })]; - const fresh = [makeGame({ gameId: 2 })]; + const response = create(Data.Response_GetGamesOfUserSchema, { + gameList: [create(Data.ServerInfo_GameSchema, { gameId: 2, description: '' })], + roomList: [], + }); const state = makeServerState({ gamesOfUser: { alice: old } }); - const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games: fresh, gametypeMap: {} }); - expect(result.gamesOfUser['alice']).toEqual(fresh); + const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', response }); + expect(result.gamesOfUser['alice']).toEqual([makeGame({ gameId: 2 })]); }); it('does not affect other users\' entries', () => { const bobGames = [makeGame({ gameId: 3 })]; + const response = create(Data.Response_GetGamesOfUserSchema, { gameList: [], roomList: [] }); const state = makeServerState({ gamesOfUser: { bob: bobGames } }); - const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', games: [], gametypeMap: {} }); + const result = serverReducer(state, { type: Types.GAMES_OF_USER, userName: 'alice', response }); expect(result.gamesOfUser['bob']).toBe(bobGames); }); }); diff --git a/webclient/src/store/server/server.reducer.ts b/webclient/src/store/server/server.reducer.ts index 44276bce6..0d2915944 100644 --- a/webclient/src/store/server/server.reducer.ts +++ b/webclient/src/store/server/server.reducer.ts @@ -1,11 +1,7 @@ -import { SortDirection, StatusEnum, UserSortField } from 'types'; -import { ServerInfo_User_UserLevelFlag } from 'generated/proto/serverinfo_user_pb'; -import type { ServerInfo_DeckStorage_Folder, ServerInfo_DeckStorage_TreeItem } from 'generated/proto/serverinfo_deckstorage_pb'; +import { App, Data } from '@app/types'; import { create } from '@bufbuild/protobuf'; -import { Response_DeckListSchema } from 'generated/proto/response_deck_list_pb'; -import { ServerInfo_DeckStorage_FolderSchema, ServerInfo_DeckStorage_TreeItemSchema } from 'generated/proto/serverinfo_deckstorage_pb'; -import { normalizeBannedUserError, normalizeGameObject, normalizeLogs, SortUtil } from '../common'; +import { normalizeBannedUserError, normalizeGameObject, normalizeGametypeMap, normalizeLogs, SortUtil } from '../common'; import { ServerAction } from './server.actions'; import { ServerState } from './server.interfaces' @@ -16,17 +12,17 @@ function splitPath(path: string): string[] { } function insertAtPath( - folder: ServerInfo_DeckStorage_Folder, + folder: Data.ServerInfo_DeckStorage_Folder, pathSegments: string[], - item: ServerInfo_DeckStorage_TreeItem, -): ServerInfo_DeckStorage_Folder { + item: Data.ServerInfo_DeckStorage_TreeItem, +): Data.ServerInfo_DeckStorage_Folder { if (pathSegments.length === 0 || (pathSegments.length === 1 && pathSegments[0] === '')) { - return create(ServerInfo_DeckStorage_FolderSchema, { items: [...folder.items, item] }); + return create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [...folder.items, item] }); } const [head, ...tail] = pathSegments; const match = folder.items.find(child => child.name === head && child.folder); if (match) { - return create(ServerInfo_DeckStorage_FolderSchema, { + return create(Data.ServerInfo_DeckStorage_FolderSchema, { items: folder.items.map(child => child === match ? { ...child, folder: insertAtPath(child.folder!, tail, item) } @@ -34,14 +30,14 @@ function insertAtPath( ), }); } - const created: ServerInfo_DeckStorage_TreeItem = create(ServerInfo_DeckStorage_TreeItemSchema, { - id: 0, name: head, folder: insertAtPath(create(ServerInfo_DeckStorage_FolderSchema, { items: [] }), tail, item) + const created: Data.ServerInfo_DeckStorage_TreeItem = create(Data.ServerInfo_DeckStorage_TreeItemSchema, { + id: 0, name: head, folder: insertAtPath(create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [] }), tail, item) }); - return create(ServerInfo_DeckStorage_FolderSchema, { items: [...folder.items, created] }); + return create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [...folder.items, created] }); } -function removeById(folder: ServerInfo_DeckStorage_Folder, id: number): ServerInfo_DeckStorage_Folder { - return create(ServerInfo_DeckStorage_FolderSchema, { +function removeById(folder: Data.ServerInfo_DeckStorage_Folder, id: number): Data.ServerInfo_DeckStorage_Folder { + return create(Data.ServerInfo_DeckStorage_FolderSchema, { items: folder.items .filter(item => item.id !== id) .map(item => @@ -50,17 +46,17 @@ function removeById(folder: ServerInfo_DeckStorage_Folder, id: number): ServerIn }); } -function removeByPath(folder: ServerInfo_DeckStorage_Folder, pathSegments: string[]): ServerInfo_DeckStorage_Folder { +function removeByPath(folder: Data.ServerInfo_DeckStorage_Folder, pathSegments: string[]): Data.ServerInfo_DeckStorage_Folder { if (pathSegments.length === 0 || (pathSegments.length === 1 && pathSegments[0] === '')) { return folder; } const [head, ...tail] = pathSegments; if (tail.length === 0) { - return create(ServerInfo_DeckStorage_FolderSchema, { + return create(Data.ServerInfo_DeckStorage_FolderSchema, { items: folder.items.filter(item => !(item.name === head && item.folder != null)) }); } - return create(ServerInfo_DeckStorage_FolderSchema, { + return create(Data.ServerInfo_DeckStorage_FolderSchema, { items: folder.items.map(item => item.name === head && item.folder ? { ...item, folder: removeByPath(item.folder, tail) } @@ -76,7 +72,7 @@ const initialState: ServerState = { status: { connectionAttemptMade: false, - state: StatusEnum.DISCONNECTED, + state: App.StatusEnum.DISCONNECTED, description: null }, info: { @@ -92,8 +88,8 @@ const initialState: ServerState = { user: null, users: [], sortUsersBy: { - field: UserSortField.NAME, - order: SortDirection.ASC + field: App.UserSortField.NAME, + order: App.SortDirection.ASC }, messages: {}, userInfo: {}, @@ -232,11 +228,19 @@ export const serverReducer = (state = initialState, action: ServerAction) => { } case Types.UPDATE_STATUS: { const { status } = action; - - return { + const newState = { ...state, - status: { ...status } + status: { ...state.status, ...status } + }; + + if (status.state === App.StatusEnum.DISCONNECTED) { + return { + ...newState, + status: { ...newState.status, connectionAttemptMade: false } + }; } + + return newState; } case Types.UPDATE_USER: case Types.ACCOUNT_EDIT_CHANGED: @@ -417,11 +421,11 @@ export const serverReducer = (state = initialState, action: ServerAction) => { } let newLevel = user.userLevel; newLevel = shouldBeMod - ? (newLevel | ServerInfo_User_UserLevelFlag.IsModerator) - : (newLevel & ~ServerInfo_User_UserLevelFlag.IsModerator); + ? (newLevel | Data.ServerInfo_User_UserLevelFlag.IsModerator) + : (newLevel & ~Data.ServerInfo_User_UserLevelFlag.IsModerator); newLevel = shouldBeJudge - ? (newLevel | ServerInfo_User_UserLevelFlag.IsJudge) - : (newLevel & ~ServerInfo_User_UserLevelFlag.IsJudge); + ? (newLevel | Data.ServerInfo_User_UserLevelFlag.IsJudge) + : (newLevel & ~Data.ServerInfo_User_UserLevelFlag.IsJudge); return { ...user, userLevel: newLevel, @@ -455,7 +459,7 @@ export const serverReducer = (state = initialState, action: ServerAction) => { } return { ...state, - backendDecks: create(Response_DeckListSchema, { + backendDecks: create(Data.Response_DeckListSchema, { root: insertAtPath(state.backendDecks.root, splitPath(action.path), action.treeItem), }), }; @@ -466,7 +470,7 @@ export const serverReducer = (state = initialState, action: ServerAction) => { } return { ...state, - backendDecks: create(Response_DeckListSchema, { + backendDecks: create(Data.Response_DeckListSchema, { root: removeById(state.backendDecks.root, action.deckId), }), }; @@ -475,12 +479,12 @@ export const serverReducer = (state = initialState, action: ServerAction) => { if (!state.backendDecks?.root) { return state; } - const newFolder: ServerInfo_DeckStorage_TreeItem = create(ServerInfo_DeckStorage_TreeItemSchema, { - id: 0, name: action.dirName, folder: create(ServerInfo_DeckStorage_FolderSchema, { items: [] }) + const newFolder: Data.ServerInfo_DeckStorage_TreeItem = create(Data.ServerInfo_DeckStorage_TreeItemSchema, { + id: 0, name: action.dirName, folder: create(Data.ServerInfo_DeckStorage_FolderSchema, { items: [] }) }); return { ...state, - backendDecks: create(Response_DeckListSchema, { + backendDecks: create(Data.Response_DeckListSchema, { root: insertAtPath(state.backendDecks.root, splitPath(action.path), newFolder), }), }; @@ -491,14 +495,17 @@ export const serverReducer = (state = initialState, action: ServerAction) => { } return { ...state, - backendDecks: create(Response_DeckListSchema, { + backendDecks: create(Data.Response_DeckListSchema, { root: removeByPath(state.backendDecks.root, splitPath(action.path)), }), }; } case Types.GAMES_OF_USER: { - const { userName, games, gametypeMap } = action; - const normalizedGames = games.map(g => normalizeGameObject(g, gametypeMap)); + const { userName, response } = action; + const gametypeMap = normalizeGametypeMap( + (response.roomList ?? []).flatMap(room => room.gametypeList ?? []) + ); + const normalizedGames = (response.gameList ?? []).map(g => normalizeGameObject(g, gametypeMap)); return { ...state, gamesOfUser: { @@ -518,7 +525,6 @@ export const serverReducer = (state = initialState, action: ServerAction) => { // Signal-only action types — no state mutation, explicit for discriminated-union exhaustiveness case Types.LOGIN_SUCCESSFUL: case Types.LOGIN_FAILED: - case Types.CONNECTION_CLOSED: case Types.CONNECTION_FAILED: case Types.TEST_CONNECTION_SUCCESSFUL: case Types.TEST_CONNECTION_FAILED: diff --git a/webclient/src/store/server/server.selectors.spec.ts b/webclient/src/store/server/server.selectors.spec.ts index f8658524f..646a8812b 100644 --- a/webclient/src/store/server/server.selectors.spec.ts +++ b/webclient/src/store/server/server.selectors.spec.ts @@ -6,7 +6,7 @@ import { makeServerState, makeUser, } from './__mocks__/server-fixtures'; -import { StatusEnum } from 'types'; +import { App } from '@app/types'; function rootState(server: ServerState) { return { server }; @@ -34,17 +34,17 @@ describe('Selectors', () => { }); it('getDescription → returns status.description', () => { - const state = makeServerState({ status: { connectionAttemptMade: false, state: StatusEnum.CONNECTED, description: 'ok' } }); + const state = makeServerState({ status: { connectionAttemptMade: false, state: App.StatusEnum.CONNECTED, description: 'ok' } }); expect(Selectors.getDescription(rootState(state))).toBe('ok'); }); it('getState → returns status.state', () => { - const state = makeServerState({ status: { connectionAttemptMade: false, state: StatusEnum.LOGGED_IN, description: null } }); - expect(Selectors.getState(rootState(state))).toBe(StatusEnum.LOGGED_IN); + const state = makeServerState({ status: { connectionAttemptMade: false, state: App.StatusEnum.LOGGED_IN, description: null } }); + expect(Selectors.getState(rootState(state))).toBe(App.StatusEnum.LOGGED_IN); }); it('getConnectionAttemptMade → returns status.connectionAttemptMade', () => { - const state = makeServerState({ status: { connectionAttemptMade: true, state: StatusEnum.DISCONNECTED, description: null } }); + const state = makeServerState({ status: { connectionAttemptMade: true, state: App.StatusEnum.DISCONNECTED, description: null } }); expect(Selectors.getConnectionAttemptMade(rootState(state))).toBe(true); }); diff --git a/webclient/src/store/server/server.types.ts b/webclient/src/store/server/server.types.ts index 89d3e7666..8a8906e4c 100644 --- a/webclient/src/store/server/server.types.ts +++ b/webclient/src/store/server/server.types.ts @@ -4,7 +4,6 @@ export const Types = { CONNECTION_ATTEMPTED: '[Server] Connection Attempted', LOGIN_SUCCESSFUL: '[Server] Login Successful', LOGIN_FAILED: '[Server] Login Failed', - CONNECTION_CLOSED: '[Server] Connection Closed', CONNECTION_FAILED: '[Server] Connection Failed', TEST_CONNECTION_SUCCESSFUL: '[Server] Test Connection Successful', TEST_CONNECTION_FAILED: '[Server] Test Connection Failed', diff --git a/webclient/src/types/app.ts b/webclient/src/types/app.ts new file mode 100644 index 000000000..2ec7c2a68 --- /dev/null +++ b/webclient/src/types/app.ts @@ -0,0 +1,8 @@ +export * from './cards'; +export * from './constants'; +export * from './countries'; +export * from './languages'; +export * from './routes'; +export * from './server'; +export * from './settings'; +export * from './sort'; diff --git a/webclient/src/types/data.ts b/webclient/src/types/data.ts new file mode 100644 index 000000000..4c0ffa673 --- /dev/null +++ b/webclient/src/types/data.ts @@ -0,0 +1 @@ +export * from '@app/generated'; diff --git a/webclient/src/types/enriched.ts b/webclient/src/types/enriched.ts new file mode 100644 index 000000000..b67aabae7 --- /dev/null +++ b/webclient/src/types/enriched.ts @@ -0,0 +1,145 @@ +import type { + Event_RoomSay, + GameEventContext, + ServerInfo_ChatMessage, + ServerInfo_Game, + ServerInfo_Room, +} from '@app/generated'; + +import { WebSocketConnectReason } from './server'; + +// ── Domain model types (proto types extended with client-side fields) ───────── + +export type Game = ServerInfo_Game & { + gameType: string; +}; + +export interface GametypeMap { [index: number]: string } + +export type Room = ServerInfo_Room & { + gametypeMap: GametypeMap; + gameList: Game[]; + order: number; +}; + +export type Message = Event_RoomSay & { + timeReceived: number; +}; + +/** + * Passed to every game event handler alongside the event payload. + * Contains per-container metadata from GameEventContainer. + * Not stored in Redux — transient routing metadata only. + */ +export interface GameEventMeta { + gameId: number; + playerId: number; + /** Raw protobuf GameEventContext object. Not stored in Redux. */ + context: GameEventContext | null; + secondsElapsed: number; + /** Proto type is uint32. Non-zero means the action was forced by a judge. */ + forcedByJudge: number; +} + +export interface LogGroups { + room: ServerInfo_ChatMessage[]; + game: ServerInfo_ChatMessage[]; + chat: ServerInfo_ChatMessage[]; +} + +// ── Connect options ─────────────────────────────────────────────────────────── +// Each variant is the enriched input for one session flow: the network +// transport fields (host/port) + the subset of proto Command_* fields the UI +// actually produces (user-entered credentials, tokens, email, etc.) + a +// `reason` discriminator so the websocket layer can route. +// +// Hand-written instead of `MessageInitShape & ...` +// because MessageInitShape is a `Message | { initShape }` union which +// collapses to the Message branding when intersected, requiring `$typeName` +// on literals. Keep these in sync with the corresponding proto command by +// convention; fields here map 1:1 to Command_* members. + +interface ConnectTransport { + host: string; + port: string; + keepalive?: number; + autojoinrooms?: boolean; + clientid?: string; +} + +export interface LoginConnectOptions extends ConnectTransport { + reason: WebSocketConnectReason.LOGIN; + userName: string; + password?: string; + hashedPassword?: string; +} + +export interface RegisterConnectOptions extends ConnectTransport { + reason: WebSocketConnectReason.REGISTER; + userName: string; + password: string; + email: string; + country: string; + realName: string; +} + +export interface ActivateConnectOptions extends ConnectTransport { + reason: WebSocketConnectReason.ACTIVATE_ACCOUNT; + userName: string; + token: string; + /** Plaintext password carried through so post-activation auto-login can hash it. */ + password?: string; +} + +export interface PasswordResetRequestConnectOptions extends ConnectTransport { + reason: WebSocketConnectReason.PASSWORD_RESET_REQUEST; + userName: string; +} + +export interface PasswordResetChallengeConnectOptions extends ConnectTransport { + reason: WebSocketConnectReason.PASSWORD_RESET_CHALLENGE; + userName: string; + email: string; +} + +export interface PasswordResetConnectOptions extends ConnectTransport { + reason: WebSocketConnectReason.PASSWORD_RESET; + userName: string; + token: string; + newPassword: string; +} + +/** + * Test connection has no proto command — it just opens and closes a socket to + * verify reachability. + */ +export interface TestConnectionOptions extends ConnectTransport { + reason: WebSocketConnectReason.TEST_CONNECTION; +} + +export type WebSocketConnectOptions = + | LoginConnectOptions + | RegisterConnectOptions + | ActivateConnectOptions + | PasswordResetRequestConnectOptions + | PasswordResetChallengeConnectOptions + | PasswordResetConnectOptions + | TestConnectionOptions; + +/** + * Context preserved through the ACCOUNT_AWAITING_ACTIVATION signal so the + * activation dialog can resubmit against the same host/user without re-entering them. + */ +export interface PendingActivationContext { + host: string; + port: string; + userName: string; +} + +/** + * Payload for the LOGIN_SUCCESSFUL signal. Only carries what the UI needs to + * persist into the selected host record (hashedPassword for "remember me"). + */ +export interface LoginSuccessContext { + hashedPassword?: string; +} diff --git a/webclient/src/types/game.ts b/webclient/src/types/game.ts deleted file mode 100644 index 8cffb3f85..000000000 --- a/webclient/src/types/game.ts +++ /dev/null @@ -1,123 +0,0 @@ -// ── Imports from generated proto files ─────────────────────────────────────── - -import type { ProtoInit } from './utilities'; -import type { GameEventContext } from 'generated/proto/game_event_context_pb'; -import type { Command_MoveCard } from 'generated/proto/command_move_card_pb'; -import type { Command_DrawCards } from 'generated/proto/command_draw_cards_pb'; -import type { Command_RollDie } from 'generated/proto/command_roll_die_pb'; -import type { Command_Shuffle } from 'generated/proto/command_shuffle_pb'; -import type { Command_FlipCard } from 'generated/proto/command_flip_card_pb'; -import type { Command_AttachCard } from 'generated/proto/command_attach_card_pb'; -import type { Command_CreateToken } from 'generated/proto/command_create_token_pb'; -import type { Command_SetCardAttr } from 'generated/proto/command_set_card_attr_pb'; -import type { Command_SetCardCounter } from 'generated/proto/command_set_card_counter_pb'; -import type { Command_IncCardCounter } from 'generated/proto/command_inc_card_counter_pb'; -import type { Command_RevealCards } from 'generated/proto/command_reveal_cards_pb'; -import type { Command_DumpZone } from 'generated/proto/command_dump_zone_pb'; -import type { Command_ChangeZoneProperties } from 'generated/proto/command_change_zone_properties_pb'; -import type { Command_CreateArrow } from 'generated/proto/command_create_arrow_pb'; -import type { Command_DeleteArrow } from 'generated/proto/command_delete_arrow_pb'; -import type { Command_CreateCounter } from 'generated/proto/command_create_counter_pb'; -import type { Command_SetCounter } from 'generated/proto/command_set_counter_pb'; -import type { Command_IncCounter } from 'generated/proto/command_inc_counter_pb'; -import type { Command_DelCounter } from 'generated/proto/command_del_counter_pb'; -import type { Command_KickFromGame } from 'generated/proto/command_kick_from_game_pb'; -import type { Command_ReadyStart } from 'generated/proto/command_ready_start_pb'; -import type { Command_Mulligan } from 'generated/proto/command_mulligan_pb'; -import type { Command_DeckSelect } from 'generated/proto/command_deck_select_pb'; -import type { Command_SetSideboardPlan } from 'generated/proto/command_set_sideboard_plan_pb'; -import type { Command_SetSideboardLock } from 'generated/proto/command_set_sideboard_lock_pb'; -import type { Command_SetActivePhase } from 'generated/proto/command_set_active_phase_pb'; -import type { Command_GameSay } from 'generated/proto/command_game_say_pb'; -import type { ServerInfo_Game } from 'generated/proto/serverinfo_game_pb'; - -// ── UI types (not proto mirrors) ────────────────────────────────────────────── - -export type Game = ServerInfo_Game & { - gameType: string; -}; - -export enum GameSortField { - START_TIME = 'startTime' -} - -export interface GameConfig { - description: string; - password: string; - maxPlayers: number; - onlyBuddies: boolean; - onlyRegistered: boolean; - spectatorsAllowed: boolean; - spectatorsNeedPassword: boolean; - spectatorsCanTalk: boolean; - spectatorsSeeEverything: boolean; - gameTypeIds: number[]; - joinAsJudge: boolean; - joinAsSpectator: boolean; - startingLifeTotal?: number; - shareDecklistsOnLoad?: boolean; -} - -export interface JoinGameParams { - gameId: number; - password: string; - spectator: boolean; - overrideRestrictions: boolean; - joinAsJudge: boolean; -} - -export enum LeaveGameReason { - OTHER = 1, - USER_KICKED = 2, - USER_LEFT = 3, - USER_DISCONNECTED = 4 -} - -// ── GameEventContext (imported for use in GameEventMeta below) ─────────────── - -/** - * Passed to every game event handler alongside the event payload. - * Contains per-container metadata from GameEventContainer. - * Not stored in Redux — transient routing metadata only. - */ -export interface GameEventMeta { - gameId: number; - playerId: number; - /** Raw protobuf GameEventContext object. Not stored in Redux. */ - context: GameEventContext | null; - secondsElapsed: number; - /** Proto type is uint32. Non-zero means the action was forced by a judge. */ - forcedByJudge: number; -} - -// ── Type aliases for generated command param types (init shapes) ────────────── -// These use ProtoInit<> because callers construct plain objects; -// the command functions internally call create(Schema, params). - -export type MoveCardParams = ProtoInit; -export type DrawCardsParams = ProtoInit; -export type RollDieParams = ProtoInit; -export type ShuffleParams = ProtoInit; -export type FlipCardParams = ProtoInit; -export type AttachCardParams = ProtoInit; -export type CreateTokenParams = ProtoInit; -export type SetCardAttrParams = ProtoInit; -export type SetCardCounterParams = ProtoInit; -export type IncCardCounterParams = ProtoInit; -export type RevealCardsParams = ProtoInit; -export type DumpZoneParams = ProtoInit; -export type ChangeZonePropertiesParams = ProtoInit; -export type CreateArrowParams = ProtoInit; -export type DeleteArrowParams = ProtoInit; -export type CreateCounterParams = ProtoInit; -export type SetCounterParams = ProtoInit; -export type IncCounterParams = ProtoInit; -export type DelCounterParams = ProtoInit; -export type KickFromGameParams = ProtoInit; -export type ReadyStartParams = ProtoInit; -export type MulliganParams = ProtoInit; -export type DeckSelectParams = ProtoInit; -export type SetSideboardPlanParams = ProtoInit; -export type SetSideboardLockParams = ProtoInit; -export type SetActivePhaseParams = ProtoInit; -export type GameSayParams = ProtoInit; diff --git a/webclient/src/types/index.ts b/webclient/src/types/index.ts index b542d7937..732981b87 100644 --- a/webclient/src/types/index.ts +++ b/webclient/src/types/index.ts @@ -1,14 +1,3 @@ -export * from './cards'; -export * from './constants'; -export * from './countries'; -export * from './game'; -export * from './room'; -export * from './server'; -export * from './sort'; -export * from './user'; -export * from './routes'; -export * from './message'; -export * from './settings'; -export * from './languages'; -export * from './logs'; -export * from './utilities'; +export * as Data from './data'; +export * as Enriched from './enriched'; +export * as App from './app'; diff --git a/webclient/src/types/logs.ts b/webclient/src/types/logs.ts deleted file mode 100644 index 3cf34b486..000000000 --- a/webclient/src/types/logs.ts +++ /dev/null @@ -1,10 +0,0 @@ -export interface LogFilters { - userName?: string; - ipAddress?: string; - gameName?: string; - gameId?: string; - message?: string; - logLocation?: string[]; - dateRange: number; - maximumResults?: number; -} diff --git a/webclient/src/types/message.ts b/webclient/src/types/message.ts deleted file mode 100644 index d4dc282a1..000000000 --- a/webclient/src/types/message.ts +++ /dev/null @@ -1,5 +0,0 @@ -import type { Event_RoomSay } from 'generated/proto/event_room_say_pb'; - -export type Message = Event_RoomSay & { - timeReceived: number; -}; diff --git a/webclient/src/types/room.ts b/webclient/src/types/room.ts deleted file mode 100644 index f4441f697..000000000 --- a/webclient/src/types/room.ts +++ /dev/null @@ -1,10 +0,0 @@ -import type { ServerInfo_Room } from 'generated/proto/serverinfo_room_pb'; -import type { Game } from './game'; - -export interface GametypeMap { [index: number]: string } - -export type Room = ServerInfo_Room & { - gametypeMap: GametypeMap; - gameList: Game[]; - order: number; -}; diff --git a/webclient/src/types/server.ts b/webclient/src/types/server.ts index 8c42b1165..6c3fa8006 100644 --- a/webclient/src/types/server.ts +++ b/webclient/src/types/server.ts @@ -1,5 +1,3 @@ -import type { ServerInfo_ChatMessage } from 'generated/proto/serverinfo_chat_message_pb'; - export interface ServerStatus { status: StatusEnum; description: string; @@ -14,23 +12,6 @@ export enum StatusEnum { DISCONNECTING = 99 } -export interface WebSocketConnectOptions { - host?: string; - port?: string; - userName?: string; - password?: string; - hashedPassword?: string; - newPassword?: string; - token?: string; - email?: string; - realName?: string; - country?: string; - autojoinrooms?: boolean; - keepalive?: number; - clientid?: string; - reason?: WebSocketConnectReason; -} - export enum WebSocketConnectReason { LOGIN, REGISTER, @@ -110,9 +91,3 @@ export const KnownHosts = { [KnownHost.ROOSTER]: { port: 4748, host: 'server.cockatrice.us', }, [KnownHost.TETRARCH]: { port: 443, host: 'mtg.tetrarch.co/servatrice' }, } - -export interface LogGroups { - room: ServerInfo_ChatMessage[]; - game: ServerInfo_ChatMessage[]; - chat: ServerInfo_ChatMessage[]; -} diff --git a/webclient/src/types/sort.ts b/webclient/src/types/sort.ts index c3312732c..4619c169b 100644 --- a/webclient/src/types/sort.ts +++ b/webclient/src/types/sort.ts @@ -3,7 +3,15 @@ export enum SortDirection { DESC = 'DESC' } -export interface SortBy { - field: string; +export interface SortBy { + field: T; order: SortDirection; } + +export enum GameSortField { + START_TIME = 'startTime' +} + +export enum UserSortField { + NAME = 'name' +} diff --git a/webclient/src/types/user.ts b/webclient/src/types/user.ts deleted file mode 100644 index 3034b977c..000000000 --- a/webclient/src/types/user.ts +++ /dev/null @@ -1,3 +0,0 @@ -export enum UserSortField { - NAME = 'name' -} diff --git a/webclient/src/types/utilities.ts b/webclient/src/types/utilities.ts deleted file mode 100644 index 9a113c264..000000000 --- a/webclient/src/types/utilities.ts +++ /dev/null @@ -1,8 +0,0 @@ -/** - * Init shape for constructing protobuf messages via create(). - * Strips $typeName and $unknown branding, making all fields optional. - * Use for function parameters that feed into create(). - */ -export type ProtoInit = { - [K in keyof T as K extends '$typeName' | '$unknown' ? never : K]?: T[K]; -}; diff --git a/webclient/src/websocket/WebClient.spec.ts b/webclient/src/websocket/WebClient.spec.ts index 799c7e552..9cb5c4671 100644 --- a/webclient/src/websocket/WebClient.spec.ts +++ b/webclient/src/websocket/WebClient.spec.ts @@ -1,10 +1,10 @@ const captured = vi.hoisted(() => ({ - wsOptions: null as any, - pbOptions: null as any, + wsOptions: null as WebSocketServiceConfig | null, + pbOptions: null as SocketTransport | null, })); vi.mock('./services/WebSocketService', () => ({ - WebSocketService: vi.fn().mockImplementation(function WebSocketServiceImpl(options: any) { + WebSocketService: vi.fn().mockImplementation(function WebSocketServiceImpl(options: WebSocketServiceConfig) { captured.wsOptions = options; return { message$: { subscribe: vi.fn() }, @@ -18,7 +18,7 @@ vi.mock('./services/WebSocketService', () => ({ })); vi.mock('./services/ProtobufService', () => ({ - ProtobufService: vi.fn().mockImplementation(function ProtobufServiceImpl(options: any) { + ProtobufService: vi.fn().mockImplementation(function ProtobufServiceImpl(options: SocketTransport) { captured.pbOptions = options; return { handleMessageEvent: vi.fn(), @@ -32,7 +32,7 @@ vi.mock('./persistence', () => ({ SessionPersistence: { clearStore: vi.fn(), initialized: vi.fn(), connectionAttempted: vi.fn() }, })); -vi.mock('store', () => ({ +vi.mock('@app/store', () => ({ GameDispatch: { clearStore: vi.fn() }, })); @@ -45,17 +45,18 @@ import { WebSocketService } from './services/WebSocketService'; import { ProtobufService } from './services/ProtobufService'; import { RoomPersistence, SessionPersistence } from './persistence'; import { ping } from './commands/session'; -import { StatusEnum } from 'types'; +import { App, Enriched } from '@app/types'; import { Subject } from 'rxjs'; import { Mock } from 'vitest'; +import { SocketTransport } from './services/ProtobufService'; +import { WebSocketServiceConfig } from './services/WebSocketService'; describe('WebClient', () => { let client: WebClient; let messageSubject: Subject; beforeEach(() => { - vi.clearAllMocks(); - (ProtobufService as Mock).mockImplementation(function ProtobufServiceImpl(options: any) { + (ProtobufService as Mock).mockImplementation(function ProtobufServiceImpl(options: SocketTransport) { captured.pbOptions = options; return { handleMessageEvent: vi.fn(), @@ -63,7 +64,7 @@ describe('WebClient', () => { }; }); messageSubject = new Subject(); - (WebSocketService as Mock).mockImplementation(function WebSocketServiceImpl(options: any) { + (WebSocketService as Mock).mockImplementation(function WebSocketServiceImpl(options: WebSocketServiceConfig) { captured.wsOptions = options; return { message$: messageSubject, @@ -97,13 +98,13 @@ describe('WebClient', () => { describe('connect', () => { it('calls SessionPersistence.connectionAttempted', () => { - const opts: any = { host: 'h', port: 1 }; + const opts: Enriched.WebSocketConnectOptions = { host: 'h', port: '1', reason: App.WebSocketConnectReason.LOGIN, userName: 'u' }; client.connect(opts); expect(SessionPersistence.connectionAttempted).toHaveBeenCalled(); }); it('stores options and calls socket.connect', () => { - const opts: any = { host: 'h', port: 1 }; + const opts: Enriched.WebSocketConnectOptions = { host: 'h', port: '1', reason: App.WebSocketConnectReason.LOGIN, userName: 'u' }; client.connect(opts); expect(client.options).toBe(opts); expect(client.socket.connect).toHaveBeenCalledWith(opts); @@ -112,7 +113,7 @@ describe('WebClient', () => { describe('testConnect', () => { it('delegates to socket.testConnect', () => { - const opts: any = { host: 'h', port: 1 }; + const opts: Enriched.WebSocketConnectOptions = { host: 'h', port: '1', reason: App.WebSocketConnectReason.LOGIN, userName: 'u' }; client.testConnect(opts); expect(client.socket.testConnect).toHaveBeenCalledWith(opts); }); @@ -127,19 +128,19 @@ describe('WebClient', () => { describe('updateStatus', () => { it('sets the status', () => { - client.updateStatus(StatusEnum.CONNECTED); - expect(client.status).toBe(StatusEnum.CONNECTED); + client.updateStatus(App.StatusEnum.CONNECTED); + expect(client.status).toBe(App.StatusEnum.CONNECTED); }); it('calls protobuf.resetCommands and clears stores on DISCONNECTED', () => { - client.updateStatus(StatusEnum.DISCONNECTED); + client.updateStatus(App.StatusEnum.DISCONNECTED); expect(client.protobuf.resetCommands).toHaveBeenCalled(); expect(RoomPersistence.clearStore).toHaveBeenCalled(); expect(SessionPersistence.clearStore).toHaveBeenCalled(); }); it('does not clear stores when status is not DISCONNECTED', () => { - client.updateStatus(StatusEnum.CONNECTED); + client.updateStatus(App.StatusEnum.CONNECTED); expect(client.protobuf.resetCommands).not.toHaveBeenCalled(); expect(RoomPersistence.clearStore).not.toHaveBeenCalled(); }); diff --git a/webclient/src/websocket/WebClient.ts b/webclient/src/websocket/WebClient.ts index 5895c653a..68df7749e 100644 --- a/webclient/src/websocket/WebClient.ts +++ b/webclient/src/websocket/WebClient.ts @@ -1,18 +1,18 @@ -import { StatusEnum, WebSocketConnectOptions } from 'types'; +import { App, Enriched } from '@app/types'; import { ProtobufService } from './services/ProtobufService'; import { WebSocketService } from './services/WebSocketService'; import { ping } from './commands/session'; -import { GameDispatch } from 'store'; +import { GameDispatch } from '@app/store'; import { RoomPersistence, SessionPersistence } from './persistence'; export class WebClient { public socket: WebSocketService; public protobuf: ProtobufService; - public options: WebSocketConnectOptions; - public status: StatusEnum; + public options: Enriched.WebSocketConnectOptions | null = null; + public status: App.StatusEnum; constructor() { this.socket = new WebSocketService({ @@ -35,13 +35,13 @@ export class WebClient { } } - public connect(options: WebSocketConnectOptions) { + public connect(options: Enriched.WebSocketConnectOptions) { SessionPersistence.connectionAttempted(); this.options = options; this.socket.connect(options); } - public testConnect(options: WebSocketConnectOptions) { + public testConnect(options: Enriched.WebSocketConnectOptions) { this.socket.testConnect(options); } @@ -49,10 +49,10 @@ export class WebClient { this.socket.disconnect(); } - public updateStatus(status: StatusEnum) { + public updateStatus(status: App.StatusEnum) { this.status = status; - if (status === StatusEnum.DISCONNECTED) { + if (status === App.StatusEnum.DISCONNECTED) { this.protobuf.resetCommands(); this.clearStores(); } diff --git a/webclient/src/websocket/commands/admin/adjustMod.ts b/webclient/src/websocket/commands/admin/adjustMod.ts index f6fba41e8..3f034c8d3 100644 --- a/webclient/src/websocket/commands/admin/adjustMod.ts +++ b/webclient/src/websocket/commands/admin/adjustMod.ts @@ -1,12 +1,16 @@ import { create } from '@bufbuild/protobuf'; +import { Data } from '@app/types'; import webClient from '../../WebClient'; -import { Command_AdjustMod_ext, Command_AdjustModSchema } from 'generated/proto/admin_commands_pb'; import { AdminPersistence } from '../../persistence'; export function adjustMod(userName: string, shouldBeMod?: boolean, shouldBeJudge?: boolean): void { - webClient.protobuf.sendAdminCommand(Command_AdjustMod_ext, create(Command_AdjustModSchema, { userName, shouldBeMod, shouldBeJudge }), { - onSuccess: () => { - AdminPersistence.adjustMod(userName, shouldBeMod, shouldBeJudge); - }, - }); + webClient.protobuf.sendAdminCommand( + Data.Command_AdjustMod_ext, + create(Data.Command_AdjustModSchema, { userName, shouldBeMod, shouldBeJudge }), + { + onSuccess: () => { + AdminPersistence.adjustMod(userName, shouldBeMod, shouldBeJudge); + }, + } + ); } diff --git a/webclient/src/websocket/commands/admin/adminCommands.spec.ts b/webclient/src/websocket/commands/admin/adminCommands.spec.ts index d3422d148..9129ee987 100644 --- a/webclient/src/websocket/commands/admin/adminCommands.spec.ts +++ b/webclient/src/websocket/commands/admin/adminCommands.spec.ts @@ -27,8 +27,6 @@ const { invokeOnSuccess } = makeCallbackHelpers( 2 ); -beforeEach(() => vi.clearAllMocks()); - // ---------------------------------------------------------------- // adjustMod // ---------------------------------------------------------------- diff --git a/webclient/src/websocket/commands/admin/reloadConfig.ts b/webclient/src/websocket/commands/admin/reloadConfig.ts index 08c92ffee..ee318eb28 100644 --- a/webclient/src/websocket/commands/admin/reloadConfig.ts +++ b/webclient/src/websocket/commands/admin/reloadConfig.ts @@ -1,10 +1,10 @@ import { create } from '@bufbuild/protobuf'; +import { Data } from '@app/types'; import webClient from '../../WebClient'; -import { Command_ReloadConfig_ext, Command_ReloadConfigSchema } from 'generated/proto/admin_commands_pb'; import { AdminPersistence } from '../../persistence'; export function reloadConfig(): void { - webClient.protobuf.sendAdminCommand(Command_ReloadConfig_ext, create(Command_ReloadConfigSchema), { + webClient.protobuf.sendAdminCommand(Data.Command_ReloadConfig_ext, create(Data.Command_ReloadConfigSchema), { onSuccess: () => { AdminPersistence.reloadConfig(); }, diff --git a/webclient/src/websocket/commands/admin/shutdownServer.ts b/webclient/src/websocket/commands/admin/shutdownServer.ts index 86cd75259..0bbb5f626 100644 --- a/webclient/src/websocket/commands/admin/shutdownServer.ts +++ b/webclient/src/websocket/commands/admin/shutdownServer.ts @@ -1,10 +1,10 @@ import { create } from '@bufbuild/protobuf'; +import { Data } from '@app/types'; import webClient from '../../WebClient'; -import { Command_ShutdownServer_ext, Command_ShutdownServerSchema } from 'generated/proto/admin_commands_pb'; import { AdminPersistence } from '../../persistence'; export function shutdownServer(reason: string, minutes: number): void { - webClient.protobuf.sendAdminCommand(Command_ShutdownServer_ext, create(Command_ShutdownServerSchema, { reason, minutes }), { + webClient.protobuf.sendAdminCommand(Data.Command_ShutdownServer_ext, create(Data.Command_ShutdownServerSchema, { reason, minutes }), { onSuccess: () => { AdminPersistence.shutdownServer(); }, diff --git a/webclient/src/websocket/commands/admin/updateServerMessage.ts b/webclient/src/websocket/commands/admin/updateServerMessage.ts index 4bd1c3f12..70e2e1eb6 100644 --- a/webclient/src/websocket/commands/admin/updateServerMessage.ts +++ b/webclient/src/websocket/commands/admin/updateServerMessage.ts @@ -1,10 +1,10 @@ import { create } from '@bufbuild/protobuf'; +import { Data } from '@app/types'; import webClient from '../../WebClient'; -import { Command_UpdateServerMessage_ext, Command_UpdateServerMessageSchema } from 'generated/proto/admin_commands_pb'; import { AdminPersistence } from '../../persistence'; export function updateServerMessage(): void { - webClient.protobuf.sendAdminCommand(Command_UpdateServerMessage_ext, create(Command_UpdateServerMessageSchema), { + webClient.protobuf.sendAdminCommand(Data.Command_UpdateServerMessage_ext, create(Data.Command_UpdateServerMessageSchema), { onSuccess: () => { AdminPersistence.updateServerMessage(); }, diff --git a/webclient/src/websocket/commands/game/attachCard.ts b/webclient/src/websocket/commands/game/attachCard.ts index 08c6b110f..4c414730c 100644 --- a/webclient/src/websocket/commands/game/attachCard.ts +++ b/webclient/src/websocket/commands/game/attachCard.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_AttachCardSchema, Command_AttachCard_ext } from 'generated/proto/command_attach_card_pb'; -import { AttachCardParams } from 'types'; -export function attachCard(gameId: number, params: AttachCardParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_AttachCard_ext, create(Command_AttachCardSchema, params)); +import { Data } from '@app/types'; + +export function attachCard(gameId: number, params: Data.AttachCardParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_AttachCard_ext, create(Data.Command_AttachCardSchema, params)); } diff --git a/webclient/src/websocket/commands/game/changeZoneProperties.ts b/webclient/src/websocket/commands/game/changeZoneProperties.ts index a35e9ec25..073a3458e 100644 --- a/webclient/src/websocket/commands/game/changeZoneProperties.ts +++ b/webclient/src/websocket/commands/game/changeZoneProperties.ts @@ -1,8 +1,12 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_ChangeZonePropertiesSchema, Command_ChangeZoneProperties_ext } from 'generated/proto/command_change_zone_properties_pb'; -import { ChangeZonePropertiesParams } from 'types'; -export function changeZoneProperties(gameId: number, params: ChangeZonePropertiesParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_ChangeZoneProperties_ext, create(Command_ChangeZonePropertiesSchema, params)); +import { Data } from '@app/types'; + +export function changeZoneProperties(gameId: number, params: Data.ChangeZonePropertiesParams): void { + webClient.protobuf.sendGameCommand( + gameId, + Data.Command_ChangeZoneProperties_ext, + create(Data.Command_ChangeZonePropertiesSchema, params) + ); } diff --git a/webclient/src/websocket/commands/game/concede.ts b/webclient/src/websocket/commands/game/concede.ts index 8aaad96b1..65f19a44f 100644 --- a/webclient/src/websocket/commands/game/concede.ts +++ b/webclient/src/websocket/commands/game/concede.ts @@ -1,7 +1,7 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_ConcedeSchema, Command_Concede_ext } from 'generated/proto/command_concede_pb'; +import { Data } from '@app/types'; export function concede(gameId: number): void { - webClient.protobuf.sendGameCommand(gameId, Command_Concede_ext, create(Command_ConcedeSchema)); + webClient.protobuf.sendGameCommand(gameId, Data.Command_Concede_ext, create(Data.Command_ConcedeSchema)); } diff --git a/webclient/src/websocket/commands/game/createArrow.ts b/webclient/src/websocket/commands/game/createArrow.ts index d85b3168e..544f0c14a 100644 --- a/webclient/src/websocket/commands/game/createArrow.ts +++ b/webclient/src/websocket/commands/game/createArrow.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_CreateArrowSchema, Command_CreateArrow_ext } from 'generated/proto/command_create_arrow_pb'; -import { CreateArrowParams } from 'types'; -export function createArrow(gameId: number, params: CreateArrowParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_CreateArrow_ext, create(Command_CreateArrowSchema, params)); +import { Data } from '@app/types'; + +export function createArrow(gameId: number, params: Data.CreateArrowParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_CreateArrow_ext, create(Data.Command_CreateArrowSchema, params)); } diff --git a/webclient/src/websocket/commands/game/createCounter.ts b/webclient/src/websocket/commands/game/createCounter.ts index 53153efc3..24c7a9ccb 100644 --- a/webclient/src/websocket/commands/game/createCounter.ts +++ b/webclient/src/websocket/commands/game/createCounter.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_CreateCounterSchema, Command_CreateCounter_ext } from 'generated/proto/command_create_counter_pb'; -import { CreateCounterParams } from 'types'; -export function createCounter(gameId: number, params: CreateCounterParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_CreateCounter_ext, create(Command_CreateCounterSchema, params)); +import { Data } from '@app/types'; + +export function createCounter(gameId: number, params: Data.CreateCounterParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_CreateCounter_ext, create(Data.Command_CreateCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/createToken.ts b/webclient/src/websocket/commands/game/createToken.ts index 06780afba..5c38f8ec6 100644 --- a/webclient/src/websocket/commands/game/createToken.ts +++ b/webclient/src/websocket/commands/game/createToken.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_CreateTokenSchema, Command_CreateToken_ext } from 'generated/proto/command_create_token_pb'; -import { CreateTokenParams } from 'types'; -export function createToken(gameId: number, params: CreateTokenParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_CreateToken_ext, create(Command_CreateTokenSchema, params)); +import { Data } from '@app/types'; + +export function createToken(gameId: number, params: Data.CreateTokenParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_CreateToken_ext, create(Data.Command_CreateTokenSchema, params)); } diff --git a/webclient/src/websocket/commands/game/deckSelect.ts b/webclient/src/websocket/commands/game/deckSelect.ts index 33905c7ac..4fd8e76c2 100644 --- a/webclient/src/websocket/commands/game/deckSelect.ts +++ b/webclient/src/websocket/commands/game/deckSelect.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_DeckSelectSchema, Command_DeckSelect_ext } from 'generated/proto/command_deck_select_pb'; -import { DeckSelectParams } from 'types'; -export function deckSelect(gameId: number, params: DeckSelectParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_DeckSelect_ext, create(Command_DeckSelectSchema, params)); +import { Data } from '@app/types'; + +export function deckSelect(gameId: number, params: Data.DeckSelectParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_DeckSelect_ext, create(Data.Command_DeckSelectSchema, params)); } diff --git a/webclient/src/websocket/commands/game/delCounter.ts b/webclient/src/websocket/commands/game/delCounter.ts index ba9e10d55..893b93056 100644 --- a/webclient/src/websocket/commands/game/delCounter.ts +++ b/webclient/src/websocket/commands/game/delCounter.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_DelCounterSchema, Command_DelCounter_ext } from 'generated/proto/command_del_counter_pb'; -import { DelCounterParams } from 'types'; -export function delCounter(gameId: number, params: DelCounterParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_DelCounter_ext, create(Command_DelCounterSchema, params)); +import { Data } from '@app/types'; + +export function delCounter(gameId: number, params: Data.DelCounterParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_DelCounter_ext, create(Data.Command_DelCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/deleteArrow.ts b/webclient/src/websocket/commands/game/deleteArrow.ts index dc15a2d00..46b952e5a 100644 --- a/webclient/src/websocket/commands/game/deleteArrow.ts +++ b/webclient/src/websocket/commands/game/deleteArrow.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_DeleteArrowSchema, Command_DeleteArrow_ext } from 'generated/proto/command_delete_arrow_pb'; -import { DeleteArrowParams } from 'types'; -export function deleteArrow(gameId: number, params: DeleteArrowParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_DeleteArrow_ext, create(Command_DeleteArrowSchema, params)); +import { Data } from '@app/types'; + +export function deleteArrow(gameId: number, params: Data.DeleteArrowParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_DeleteArrow_ext, create(Data.Command_DeleteArrowSchema, params)); } diff --git a/webclient/src/websocket/commands/game/drawCards.ts b/webclient/src/websocket/commands/game/drawCards.ts index 1ba96ca8f..2ec92ee5f 100644 --- a/webclient/src/websocket/commands/game/drawCards.ts +++ b/webclient/src/websocket/commands/game/drawCards.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_DrawCardsSchema, Command_DrawCards_ext } from 'generated/proto/command_draw_cards_pb'; -import { DrawCardsParams } from 'types'; -export function drawCards(gameId: number, params: DrawCardsParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_DrawCards_ext, create(Command_DrawCardsSchema, params)); +import { Data } from '@app/types'; + +export function drawCards(gameId: number, params: Data.DrawCardsParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_DrawCards_ext, create(Data.Command_DrawCardsSchema, params)); } diff --git a/webclient/src/websocket/commands/game/dumpZone.ts b/webclient/src/websocket/commands/game/dumpZone.ts index 3cee63e7e..5de3ed76c 100644 --- a/webclient/src/websocket/commands/game/dumpZone.ts +++ b/webclient/src/websocket/commands/game/dumpZone.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_DumpZoneSchema, Command_DumpZone_ext } from 'generated/proto/command_dump_zone_pb'; -import { DumpZoneParams } from 'types'; -export function dumpZone(gameId: number, params: DumpZoneParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_DumpZone_ext, create(Command_DumpZoneSchema, params)); +import { Data } from '@app/types'; + +export function dumpZone(gameId: number, params: Data.DumpZoneParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_DumpZone_ext, create(Data.Command_DumpZoneSchema, params)); } diff --git a/webclient/src/websocket/commands/game/flipCard.ts b/webclient/src/websocket/commands/game/flipCard.ts index b8d1130cf..598874115 100644 --- a/webclient/src/websocket/commands/game/flipCard.ts +++ b/webclient/src/websocket/commands/game/flipCard.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_FlipCardSchema, Command_FlipCard_ext } from 'generated/proto/command_flip_card_pb'; -import { FlipCardParams } from 'types'; -export function flipCard(gameId: number, params: FlipCardParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_FlipCard_ext, create(Command_FlipCardSchema, params)); +import { Data } from '@app/types'; + +export function flipCard(gameId: number, params: Data.FlipCardParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_FlipCard_ext, create(Data.Command_FlipCardSchema, params)); } diff --git a/webclient/src/websocket/commands/game/gameCommands.spec.ts b/webclient/src/websocket/commands/game/gameCommands.spec.ts index ddb671f88..ea32bb658 100644 --- a/webclient/src/websocket/commands/game/gameCommands.spec.ts +++ b/webclient/src/websocket/commands/game/gameCommands.spec.ts @@ -1,37 +1,7 @@ import webClient from '../../WebClient'; import { create, setExtension } from '@bufbuild/protobuf'; -import { GameCommandSchema, Command_Judge_ext } from 'generated/proto/game_commands_pb'; -import { Command_DrawCardsSchema, Command_DrawCards_ext } from 'generated/proto/command_draw_cards_pb'; -import { Command_AttachCard_ext } from 'generated/proto/command_attach_card_pb'; -import { Command_ChangeZoneProperties_ext } from 'generated/proto/command_change_zone_properties_pb'; -import { Command_Concede_ext, Command_Unconcede_ext } from 'generated/proto/command_concede_pb'; -import { Command_CreateArrow_ext } from 'generated/proto/command_create_arrow_pb'; -import { Command_CreateCounter_ext } from 'generated/proto/command_create_counter_pb'; -import { Command_CreateToken_ext } from 'generated/proto/command_create_token_pb'; -import { Command_DeckSelect_ext } from 'generated/proto/command_deck_select_pb'; -import { Command_DelCounter_ext } from 'generated/proto/command_del_counter_pb'; -import { Command_DeleteArrow_ext } from 'generated/proto/command_delete_arrow_pb'; -import { Command_DumpZone_ext } from 'generated/proto/command_dump_zone_pb'; -import { Command_FlipCard_ext } from 'generated/proto/command_flip_card_pb'; -import { Command_GameSay_ext } from 'generated/proto/command_game_say_pb'; -import { Command_IncCardCounter_ext } from 'generated/proto/command_inc_card_counter_pb'; -import { Command_IncCounter_ext } from 'generated/proto/command_inc_counter_pb'; -import { Command_KickFromGame_ext } from 'generated/proto/command_kick_from_game_pb'; -import { Command_LeaveGame_ext } from 'generated/proto/command_leave_game_pb'; -import { Command_MoveCard_ext } from 'generated/proto/command_move_card_pb'; -import { Command_Mulligan_ext } from 'generated/proto/command_mulligan_pb'; -import { Command_NextTurn_ext } from 'generated/proto/command_next_turn_pb'; -import { Command_ReadyStart_ext } from 'generated/proto/command_ready_start_pb'; -import { Command_RevealCards_ext } from 'generated/proto/command_reveal_cards_pb'; -import { Command_ReverseTurn_ext } from 'generated/proto/command_reverse_turn_pb'; -import { Command_SetActivePhase_ext } from 'generated/proto/command_set_active_phase_pb'; -import { Command_SetCardAttr_ext } from 'generated/proto/command_set_card_attr_pb'; -import { Command_SetCardCounter_ext } from 'generated/proto/command_set_card_counter_pb'; -import { Command_SetCounter_ext } from 'generated/proto/command_set_counter_pb'; -import { Command_SetSideboardLock_ext } from 'generated/proto/command_set_sideboard_lock_pb'; -import { Command_SetSideboardPlan_ext } from 'generated/proto/command_set_sideboard_plan_pb'; -import { Command_Shuffle_ext } from 'generated/proto/command_shuffle_pb'; -import { Command_UndoDraw_ext } from 'generated/proto/command_undo_draw_pb'; +import { Data } from '@app/types'; + import { attachCard } from './attachCard'; import { changeZoneProperties } from './changeZoneProperties'; import { concede } from './concede'; @@ -73,128 +43,126 @@ vi.mock('../../WebClient', () => ({ const gameId = 1; -import { Mock } from 'vitest'; - -beforeEach(() => { - (webClient.protobuf.sendGameCommand as Mock).mockClear(); -}); - describe('Game commands — delegate to webClient.protobuf.sendGameCommand', () => { it('attachCard sends Command_AttachCard', () => { attachCard(gameId, { cardId: 10, startZone: 'hand' }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_AttachCard_ext, expect.objectContaining({ cardId: 10, startZone: 'hand' }) + gameId, Data.Command_AttachCard_ext, expect.objectContaining({ cardId: 10, startZone: 'hand' }) ); }); it('changeZoneProperties sends Command_ChangeZoneProperties', () => { changeZoneProperties(gameId, { zoneName: 'side' }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_ChangeZoneProperties_ext, expect.objectContaining({ zoneName: 'side' }) + gameId, Data.Command_ChangeZoneProperties_ext, expect.objectContaining({ zoneName: 'side' }) ); }); it('concede sends Command_Concede with empty object', () => { concede(gameId); - expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_Concede_ext, expect.any(Object)); + expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Data.Command_Concede_ext, expect.any(Object)); }); it('createArrow sends Command_CreateArrow', () => { createArrow(gameId, { startPlayerId: 1, startZone: 'hand' }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_CreateArrow_ext, expect.objectContaining({ startPlayerId: 1, startZone: 'hand' }) + gameId, Data.Command_CreateArrow_ext, expect.objectContaining({ startPlayerId: 1, startZone: 'hand' }) ); }); it('createCounter sends Command_CreateCounter', () => { createCounter(gameId, { counterName: 'life' }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_CreateCounter_ext, expect.objectContaining({ counterName: 'life' }) + gameId, Data.Command_CreateCounter_ext, expect.objectContaining({ counterName: 'life' }) ); }); it('createToken sends Command_CreateToken', () => { createToken(gameId, { cardName: 'Goblin', zone: 'play' }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_CreateToken_ext, expect.objectContaining({ cardName: 'Goblin', zone: 'play' }) + gameId, Data.Command_CreateToken_ext, expect.objectContaining({ cardName: 'Goblin', zone: 'play' }) ); }); it('deckSelect sends Command_DeckSelect', () => { deckSelect(gameId, { deckId: 5 }); - expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_DeckSelect_ext, expect.objectContaining({ deckId: 5 })); + expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( + gameId, Data.Command_DeckSelect_ext, expect.objectContaining({ deckId: 5 }) + ); }); it('delCounter sends Command_DelCounter', () => { delCounter(gameId, { counterId: 3 }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_DelCounter_ext, expect.objectContaining({ counterId: 3 }) + gameId, Data.Command_DelCounter_ext, expect.objectContaining({ counterId: 3 }) ); }); it('deleteArrow sends Command_DeleteArrow', () => { deleteArrow(gameId, { arrowId: 2 }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_DeleteArrow_ext, expect.objectContaining({ arrowId: 2 }) + gameId, Data.Command_DeleteArrow_ext, expect.objectContaining({ arrowId: 2 }) ); }); it('drawCards sends Command_DrawCards', () => { drawCards(gameId, { number: 3 }); - expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_DrawCards_ext, expect.objectContaining({ number: 3 })); + expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( + gameId, Data.Command_DrawCards_ext, expect.objectContaining({ number: 3 }) + ); }); it('dumpZone sends Command_DumpZone', () => { dumpZone(gameId, { playerId: 2, zoneName: 'library' }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_DumpZone_ext, expect.objectContaining({ playerId: 2, zoneName: 'library' }) + gameId, Data.Command_DumpZone_ext, expect.objectContaining({ playerId: 2, zoneName: 'library' }) ); }); it('flipCard sends Command_FlipCard', () => { flipCard(gameId, { cardId: 7, faceDown: false }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_FlipCard_ext, expect.objectContaining({ cardId: 7, faceDown: false }) + gameId, Data.Command_FlipCard_ext, expect.objectContaining({ cardId: 7, faceDown: false }) ); }); it('gameSay sends Command_GameSay', () => { gameSay(gameId, { message: 'hello' }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_GameSay_ext, expect.objectContaining({ message: 'hello' }) + gameId, Data.Command_GameSay_ext, expect.objectContaining({ message: 'hello' }) ); }); it('incCardCounter sends Command_IncCardCounter', () => { incCardCounter(gameId, { cardId: 5, counterId: 1 }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_IncCardCounter_ext, expect.objectContaining({ cardId: 5, counterId: 1 }) + gameId, Data.Command_IncCardCounter_ext, expect.objectContaining({ cardId: 5, counterId: 1 }) ); }); it('incCounter sends Command_IncCounter', () => { incCounter(gameId, { counterId: 1, delta: 5 }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_IncCounter_ext, expect.objectContaining({ counterId: 1, delta: 5 }) + gameId, Data.Command_IncCounter_ext, expect.objectContaining({ counterId: 1, delta: 5 }) ); }); it('kickFromGame sends Command_KickFromGame', () => { kickFromGame(gameId, { playerId: 2 }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_KickFromGame_ext, expect.objectContaining({ playerId: 2 }) + gameId, Data.Command_KickFromGame_ext, expect.objectContaining({ playerId: 2 }) ); }); it('leaveGame sends Command_LeaveGame with empty object', () => { leaveGame(gameId); - expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_LeaveGame_ext, expect.any(Object)); + expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Data.Command_LeaveGame_ext, expect.any(Object)); }); it('moveCard sends Command_MoveCard', () => { moveCard(gameId, { startZone: 'hand', targetZone: 'graveyard' }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_MoveCard_ext, + gameId, Data.Command_MoveCard_ext, expect.objectContaining({ startZone: 'hand', targetZone: 'graveyard' }) ); }); @@ -202,45 +170,45 @@ describe('Game commands — delegate to webClient.protobuf.sendGameCommand', () it('mulligan sends Command_Mulligan', () => { mulligan(gameId, { number: 7 }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_Mulligan_ext, expect.objectContaining({ number: 7 }) + gameId, Data.Command_Mulligan_ext, expect.objectContaining({ number: 7 }) ); }); it('nextTurn sends Command_NextTurn with empty object', () => { nextTurn(gameId); - expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_NextTurn_ext, expect.any(Object)); + expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Data.Command_NextTurn_ext, expect.any(Object)); }); it('readyStart sends Command_ReadyStart', () => { readyStart(gameId, { ready: true }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_ReadyStart_ext, expect.objectContaining({ ready: true }) + gameId, Data.Command_ReadyStart_ext, expect.objectContaining({ ready: true }) ); }); it('revealCards sends Command_RevealCards', () => { revealCards(gameId, { zoneName: 'hand', cardId: [1, 2] }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_RevealCards_ext, expect.objectContaining({ zoneName: 'hand', cardId: [1, 2] }) + gameId, Data.Command_RevealCards_ext, expect.objectContaining({ zoneName: 'hand', cardId: [1, 2] }) ); }); it('reverseTurn sends Command_ReverseTurn with empty object', () => { reverseTurn(gameId); - expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_ReverseTurn_ext, expect.any(Object)); + expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Data.Command_ReverseTurn_ext, expect.any(Object)); }); it('setActivePhase sends Command_SetActivePhase', () => { setActivePhase(gameId, { phase: 2 }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_SetActivePhase_ext, expect.objectContaining({ phase: 2 }) + gameId, Data.Command_SetActivePhase_ext, expect.objectContaining({ phase: 2 }) ); }); it('setCardAttr sends Command_SetCardAttr', () => { setCardAttr(gameId, { zone: 'play', cardId: 5, attrValue: '2' }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_SetCardAttr_ext, + gameId, Data.Command_SetCardAttr_ext, expect.objectContaining({ zone: 'play', cardId: 5, attrValue: '2' }) ); }); @@ -248,56 +216,56 @@ describe('Game commands — delegate to webClient.protobuf.sendGameCommand', () it('setCardCounter sends Command_SetCardCounter', () => { setCardCounter(gameId, { cardId: 5, counterId: 1 }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_SetCardCounter_ext, expect.objectContaining({ cardId: 5, counterId: 1 }) + gameId, Data.Command_SetCardCounter_ext, expect.objectContaining({ cardId: 5, counterId: 1 }) ); }); it('setCounter sends Command_SetCounter', () => { setCounter(gameId, { counterId: 1, value: 10 }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_SetCounter_ext, expect.objectContaining({ counterId: 1, value: 10 }) + gameId, Data.Command_SetCounter_ext, expect.objectContaining({ counterId: 1, value: 10 }) ); }); it('setSideboardLock sends Command_SetSideboardLock', () => { setSideboardLock(gameId, { locked: true }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_SetSideboardLock_ext, expect.objectContaining({ locked: true }) + gameId, Data.Command_SetSideboardLock_ext, expect.objectContaining({ locked: true }) ); }); it('setSideboardPlan sends Command_SetSideboardPlan', () => { setSideboardPlan(gameId, { moveList: [] }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_SetSideboardPlan_ext, expect.objectContaining({ moveList: expect.any(Array) }) + gameId, Data.Command_SetSideboardPlan_ext, expect.objectContaining({ moveList: expect.any(Array) }) ); }); it('shuffle sends Command_Shuffle', () => { shuffle(gameId, { zoneName: 'hand' }); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( - gameId, Command_Shuffle_ext, expect.objectContaining({ zoneName: 'hand' }) + gameId, Data.Command_Shuffle_ext, expect.objectContaining({ zoneName: 'hand' }) ); }); it('undoDraw sends Command_UndoDraw with empty object', () => { undoDraw(gameId); - expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_UndoDraw_ext, expect.any(Object)); + expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Data.Command_UndoDraw_ext, expect.any(Object)); }); it('unconcede sends Command_Unconcede with empty object', () => { unconcede(gameId); - expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Command_Unconcede_ext, expect.any(Object)); + expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith(gameId, Data.Command_Unconcede_ext, expect.any(Object)); }); it('judge sends Command_Judge with targetId and wrapped gameCommand array', () => { const targetId = 3; - const innerCmd = create(GameCommandSchema); - setExtension(innerCmd, Command_DrawCards_ext, create(Command_DrawCardsSchema, { number: 2 })); + const innerCmd = create(Data.GameCommandSchema); + setExtension(innerCmd, Data.Command_DrawCards_ext, create(Data.Command_DrawCardsSchema, { number: 2 })); judge(gameId, targetId, innerCmd); expect(webClient.protobuf.sendGameCommand).toHaveBeenCalledWith( gameId, - Command_Judge_ext, + Data.Command_Judge_ext, expect.objectContaining({ targetId: 3, gameCommand: expect.any(Array) }) ); }); diff --git a/webclient/src/websocket/commands/game/gameSay.ts b/webclient/src/websocket/commands/game/gameSay.ts index 86a798f9c..44ca6479d 100644 --- a/webclient/src/websocket/commands/game/gameSay.ts +++ b/webclient/src/websocket/commands/game/gameSay.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_GameSaySchema, Command_GameSay_ext } from 'generated/proto/command_game_say_pb'; -import { GameSayParams } from 'types'; -export function gameSay(gameId: number, params: GameSayParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_GameSay_ext, create(Command_GameSaySchema, params)); +import { Data } from '@app/types'; + +export function gameSay(gameId: number, params: Data.GameSayParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_GameSay_ext, create(Data.Command_GameSaySchema, params)); } diff --git a/webclient/src/websocket/commands/game/incCardCounter.ts b/webclient/src/websocket/commands/game/incCardCounter.ts index 6a72d7baa..906622d3e 100644 --- a/webclient/src/websocket/commands/game/incCardCounter.ts +++ b/webclient/src/websocket/commands/game/incCardCounter.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_IncCardCounterSchema, Command_IncCardCounter_ext } from 'generated/proto/command_inc_card_counter_pb'; -import { IncCardCounterParams } from 'types'; -export function incCardCounter(gameId: number, params: IncCardCounterParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_IncCardCounter_ext, create(Command_IncCardCounterSchema, params)); +import { Data } from '@app/types'; + +export function incCardCounter(gameId: number, params: Data.IncCardCounterParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_IncCardCounter_ext, create(Data.Command_IncCardCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/incCounter.ts b/webclient/src/websocket/commands/game/incCounter.ts index f00a358c9..2ef136e03 100644 --- a/webclient/src/websocket/commands/game/incCounter.ts +++ b/webclient/src/websocket/commands/game/incCounter.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_IncCounterSchema, Command_IncCounter_ext } from 'generated/proto/command_inc_counter_pb'; -import { IncCounterParams } from 'types'; -export function incCounter(gameId: number, params: IncCounterParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_IncCounter_ext, create(Command_IncCounterSchema, params)); +import { Data } from '@app/types'; + +export function incCounter(gameId: number, params: Data.IncCounterParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_IncCounter_ext, create(Data.Command_IncCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/judge.ts b/webclient/src/websocket/commands/game/judge.ts index 5142131bb..634392dd6 100644 --- a/webclient/src/websocket/commands/game/judge.ts +++ b/webclient/src/websocket/commands/game/judge.ts @@ -1,10 +1,9 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_JudgeSchema, Command_Judge_ext } from 'generated/proto/game_commands_pb'; -import type { GameCommand } from 'generated/proto/game_commands_pb'; +import { Data } from '@app/types'; -export function judge(gameId: number, targetId: number, innerGameCommand: GameCommand): void { - webClient.protobuf.sendGameCommand(gameId, Command_Judge_ext, create(Command_JudgeSchema, { +export function judge(gameId: number, targetId: number, innerGameCommand: Data.GameCommand): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_Judge_ext, create(Data.Command_JudgeSchema, { targetId, gameCommand: [innerGameCommand], })); diff --git a/webclient/src/websocket/commands/game/kickFromGame.ts b/webclient/src/websocket/commands/game/kickFromGame.ts index ef41a755c..e6214478d 100644 --- a/webclient/src/websocket/commands/game/kickFromGame.ts +++ b/webclient/src/websocket/commands/game/kickFromGame.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_KickFromGameSchema, Command_KickFromGame_ext } from 'generated/proto/command_kick_from_game_pb'; -import { KickFromGameParams } from 'types'; -export function kickFromGame(gameId: number, params: KickFromGameParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_KickFromGame_ext, create(Command_KickFromGameSchema, params)); +import { Data } from '@app/types'; + +export function kickFromGame(gameId: number, params: Data.KickFromGameParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_KickFromGame_ext, create(Data.Command_KickFromGameSchema, params)); } diff --git a/webclient/src/websocket/commands/game/leaveGame.ts b/webclient/src/websocket/commands/game/leaveGame.ts index f46503510..28fa7f0da 100644 --- a/webclient/src/websocket/commands/game/leaveGame.ts +++ b/webclient/src/websocket/commands/game/leaveGame.ts @@ -1,7 +1,7 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_LeaveGameSchema, Command_LeaveGame_ext } from 'generated/proto/command_leave_game_pb'; +import { Data } from '@app/types'; export function leaveGame(gameId: number): void { - webClient.protobuf.sendGameCommand(gameId, Command_LeaveGame_ext, create(Command_LeaveGameSchema)); + webClient.protobuf.sendGameCommand(gameId, Data.Command_LeaveGame_ext, create(Data.Command_LeaveGameSchema)); } diff --git a/webclient/src/websocket/commands/game/moveCard.ts b/webclient/src/websocket/commands/game/moveCard.ts index 400a6171c..eb547531c 100644 --- a/webclient/src/websocket/commands/game/moveCard.ts +++ b/webclient/src/websocket/commands/game/moveCard.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_MoveCardSchema, Command_MoveCard_ext } from 'generated/proto/command_move_card_pb'; -import { MoveCardParams } from 'types'; -export function moveCard(gameId: number, params: MoveCardParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_MoveCard_ext, create(Command_MoveCardSchema, params)); +import { Data } from '@app/types'; + +export function moveCard(gameId: number, params: Data.MoveCardParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_MoveCard_ext, create(Data.Command_MoveCardSchema, params)); } diff --git a/webclient/src/websocket/commands/game/mulligan.ts b/webclient/src/websocket/commands/game/mulligan.ts index d9285ddfc..21b1bbd69 100644 --- a/webclient/src/websocket/commands/game/mulligan.ts +++ b/webclient/src/websocket/commands/game/mulligan.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_MulliganSchema, Command_Mulligan_ext } from 'generated/proto/command_mulligan_pb'; -import { MulliganParams } from 'types'; -export function mulligan(gameId: number, params: MulliganParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_Mulligan_ext, create(Command_MulliganSchema, params)); +import { Data } from '@app/types'; + +export function mulligan(gameId: number, params: Data.MulliganParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_Mulligan_ext, create(Data.Command_MulliganSchema, params)); } diff --git a/webclient/src/websocket/commands/game/nextTurn.ts b/webclient/src/websocket/commands/game/nextTurn.ts index 70b332ef2..892ddc4e7 100644 --- a/webclient/src/websocket/commands/game/nextTurn.ts +++ b/webclient/src/websocket/commands/game/nextTurn.ts @@ -1,7 +1,7 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_NextTurnSchema, Command_NextTurn_ext } from 'generated/proto/command_next_turn_pb'; +import { Data } from '@app/types'; export function nextTurn(gameId: number): void { - webClient.protobuf.sendGameCommand(gameId, Command_NextTurn_ext, create(Command_NextTurnSchema)); + webClient.protobuf.sendGameCommand(gameId, Data.Command_NextTurn_ext, create(Data.Command_NextTurnSchema)); } diff --git a/webclient/src/websocket/commands/game/readyStart.ts b/webclient/src/websocket/commands/game/readyStart.ts index def0a445e..0ec2f0461 100644 --- a/webclient/src/websocket/commands/game/readyStart.ts +++ b/webclient/src/websocket/commands/game/readyStart.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_ReadyStartSchema, Command_ReadyStart_ext } from 'generated/proto/command_ready_start_pb'; -import { ReadyStartParams } from 'types'; -export function readyStart(gameId: number, params: ReadyStartParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_ReadyStart_ext, create(Command_ReadyStartSchema, params)); +import { Data } from '@app/types'; + +export function readyStart(gameId: number, params: Data.ReadyStartParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_ReadyStart_ext, create(Data.Command_ReadyStartSchema, params)); } diff --git a/webclient/src/websocket/commands/game/revealCards.ts b/webclient/src/websocket/commands/game/revealCards.ts index aaa860928..168c91269 100644 --- a/webclient/src/websocket/commands/game/revealCards.ts +++ b/webclient/src/websocket/commands/game/revealCards.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_RevealCardsSchema, Command_RevealCards_ext } from 'generated/proto/command_reveal_cards_pb'; -import { RevealCardsParams } from 'types'; -export function revealCards(gameId: number, params: RevealCardsParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_RevealCards_ext, create(Command_RevealCardsSchema, params)); +import { Data } from '@app/types'; + +export function revealCards(gameId: number, params: Data.RevealCardsParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_RevealCards_ext, create(Data.Command_RevealCardsSchema, params)); } diff --git a/webclient/src/websocket/commands/game/reverseTurn.ts b/webclient/src/websocket/commands/game/reverseTurn.ts index e9be52a5b..89d9f0ac4 100644 --- a/webclient/src/websocket/commands/game/reverseTurn.ts +++ b/webclient/src/websocket/commands/game/reverseTurn.ts @@ -1,7 +1,7 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_ReverseTurnSchema, Command_ReverseTurn_ext } from 'generated/proto/command_reverse_turn_pb'; +import { Data } from '@app/types'; export function reverseTurn(gameId: number): void { - webClient.protobuf.sendGameCommand(gameId, Command_ReverseTurn_ext, create(Command_ReverseTurnSchema)); + webClient.protobuf.sendGameCommand(gameId, Data.Command_ReverseTurn_ext, create(Data.Command_ReverseTurnSchema)); } diff --git a/webclient/src/websocket/commands/game/setActivePhase.ts b/webclient/src/websocket/commands/game/setActivePhase.ts index 9ccb04284..d936f09f7 100644 --- a/webclient/src/websocket/commands/game/setActivePhase.ts +++ b/webclient/src/websocket/commands/game/setActivePhase.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_SetActivePhaseSchema, Command_SetActivePhase_ext } from 'generated/proto/command_set_active_phase_pb'; -import { SetActivePhaseParams } from 'types'; -export function setActivePhase(gameId: number, params: SetActivePhaseParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_SetActivePhase_ext, create(Command_SetActivePhaseSchema, params)); +import { Data } from '@app/types'; + +export function setActivePhase(gameId: number, params: Data.SetActivePhaseParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_SetActivePhase_ext, create(Data.Command_SetActivePhaseSchema, params)); } diff --git a/webclient/src/websocket/commands/game/setCardAttr.ts b/webclient/src/websocket/commands/game/setCardAttr.ts index 5e7e8c57f..1440712a7 100644 --- a/webclient/src/websocket/commands/game/setCardAttr.ts +++ b/webclient/src/websocket/commands/game/setCardAttr.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_SetCardAttrSchema, Command_SetCardAttr_ext } from 'generated/proto/command_set_card_attr_pb'; -import { SetCardAttrParams } from 'types'; -export function setCardAttr(gameId: number, params: SetCardAttrParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_SetCardAttr_ext, create(Command_SetCardAttrSchema, params)); +import { Data } from '@app/types'; + +export function setCardAttr(gameId: number, params: Data.SetCardAttrParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_SetCardAttr_ext, create(Data.Command_SetCardAttrSchema, params)); } diff --git a/webclient/src/websocket/commands/game/setCardCounter.ts b/webclient/src/websocket/commands/game/setCardCounter.ts index 18a8a991f..a889f6bea 100644 --- a/webclient/src/websocket/commands/game/setCardCounter.ts +++ b/webclient/src/websocket/commands/game/setCardCounter.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_SetCardCounterSchema, Command_SetCardCounter_ext } from 'generated/proto/command_set_card_counter_pb'; -import { SetCardCounterParams } from 'types'; -export function setCardCounter(gameId: number, params: SetCardCounterParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_SetCardCounter_ext, create(Command_SetCardCounterSchema, params)); +import { Data } from '@app/types'; + +export function setCardCounter(gameId: number, params: Data.SetCardCounterParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_SetCardCounter_ext, create(Data.Command_SetCardCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/setCounter.ts b/webclient/src/websocket/commands/game/setCounter.ts index 579561d62..bf54d0fda 100644 --- a/webclient/src/websocket/commands/game/setCounter.ts +++ b/webclient/src/websocket/commands/game/setCounter.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_SetCounterSchema, Command_SetCounter_ext } from 'generated/proto/command_set_counter_pb'; -import { SetCounterParams } from 'types'; -export function setCounter(gameId: number, params: SetCounterParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_SetCounter_ext, create(Command_SetCounterSchema, params)); +import { Data } from '@app/types'; + +export function setCounter(gameId: number, params: Data.SetCounterParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_SetCounter_ext, create(Data.Command_SetCounterSchema, params)); } diff --git a/webclient/src/websocket/commands/game/setSideboardLock.ts b/webclient/src/websocket/commands/game/setSideboardLock.ts index ff639a461..16b86c024 100644 --- a/webclient/src/websocket/commands/game/setSideboardLock.ts +++ b/webclient/src/websocket/commands/game/setSideboardLock.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_SetSideboardLockSchema, Command_SetSideboardLock_ext } from 'generated/proto/command_set_sideboard_lock_pb'; -import { SetSideboardLockParams } from 'types'; -export function setSideboardLock(gameId: number, params: SetSideboardLockParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_SetSideboardLock_ext, create(Command_SetSideboardLockSchema, params)); +import { Data } from '@app/types'; + +export function setSideboardLock(gameId: number, params: Data.SetSideboardLockParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_SetSideboardLock_ext, create(Data.Command_SetSideboardLockSchema, params)); } diff --git a/webclient/src/websocket/commands/game/setSideboardPlan.ts b/webclient/src/websocket/commands/game/setSideboardPlan.ts index d69d85c89..5f08d30f7 100644 --- a/webclient/src/websocket/commands/game/setSideboardPlan.ts +++ b/webclient/src/websocket/commands/game/setSideboardPlan.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_SetSideboardPlanSchema, Command_SetSideboardPlan_ext } from 'generated/proto/command_set_sideboard_plan_pb'; -import { SetSideboardPlanParams } from 'types'; -export function setSideboardPlan(gameId: number, params: SetSideboardPlanParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_SetSideboardPlan_ext, create(Command_SetSideboardPlanSchema, params)); +import { Data } from '@app/types'; + +export function setSideboardPlan(gameId: number, params: Data.SetSideboardPlanParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_SetSideboardPlan_ext, create(Data.Command_SetSideboardPlanSchema, params)); } diff --git a/webclient/src/websocket/commands/game/shuffle.ts b/webclient/src/websocket/commands/game/shuffle.ts index 943ad4a57..b91d5706a 100644 --- a/webclient/src/websocket/commands/game/shuffle.ts +++ b/webclient/src/websocket/commands/game/shuffle.ts @@ -1,8 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_ShuffleSchema, Command_Shuffle_ext } from 'generated/proto/command_shuffle_pb'; -import { ShuffleParams } from 'types'; -export function shuffle(gameId: number, params: ShuffleParams): void { - webClient.protobuf.sendGameCommand(gameId, Command_Shuffle_ext, create(Command_ShuffleSchema, params)); +import { Data } from '@app/types'; + +export function shuffle(gameId: number, params: Data.ShuffleParams): void { + webClient.protobuf.sendGameCommand(gameId, Data.Command_Shuffle_ext, create(Data.Command_ShuffleSchema, params)); } diff --git a/webclient/src/websocket/commands/game/unconcede.ts b/webclient/src/websocket/commands/game/unconcede.ts index e6888f7ce..e9c4a4c7a 100644 --- a/webclient/src/websocket/commands/game/unconcede.ts +++ b/webclient/src/websocket/commands/game/unconcede.ts @@ -1,7 +1,7 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_UnconcedeSchema, Command_Unconcede_ext } from 'generated/proto/command_concede_pb'; +import { Data } from '@app/types'; export function unconcede(gameId: number): void { - webClient.protobuf.sendGameCommand(gameId, Command_Unconcede_ext, create(Command_UnconcedeSchema)); + webClient.protobuf.sendGameCommand(gameId, Data.Command_Unconcede_ext, create(Data.Command_UnconcedeSchema)); } diff --git a/webclient/src/websocket/commands/game/undoDraw.ts b/webclient/src/websocket/commands/game/undoDraw.ts index dd0a3358f..f8f820b87 100644 --- a/webclient/src/websocket/commands/game/undoDraw.ts +++ b/webclient/src/websocket/commands/game/undoDraw.ts @@ -1,7 +1,7 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_UndoDrawSchema, Command_UndoDraw_ext } from 'generated/proto/command_undo_draw_pb'; +import { Data } from '@app/types'; export function undoDraw(gameId: number): void { - webClient.protobuf.sendGameCommand(gameId, Command_UndoDraw_ext, create(Command_UndoDrawSchema)); + webClient.protobuf.sendGameCommand(gameId, Data.Command_UndoDraw_ext, create(Data.Command_UndoDrawSchema)); } diff --git a/webclient/src/websocket/commands/moderator/banFromServer.ts b/webclient/src/websocket/commands/moderator/banFromServer.ts index c0227fddd..12f04dd9f 100644 --- a/webclient/src/websocket/commands/moderator/banFromServer.ts +++ b/webclient/src/websocket/commands/moderator/banFromServer.ts @@ -1,11 +1,12 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_BanFromServer_ext, Command_BanFromServerSchema } from 'generated/proto/moderator_commands_pb'; + import { ModeratorPersistence } from '../../persistence'; +import { Data } from '@app/types'; export function banFromServer(minutes: number, userName?: string, address?: string, reason?: string, visibleReason?: string, clientid?: string, removeMessages?: number): void { - webClient.protobuf.sendModeratorCommand(Command_BanFromServer_ext, create(Command_BanFromServerSchema, { + webClient.protobuf.sendModeratorCommand(Data.Command_BanFromServer_ext, create(Data.Command_BanFromServerSchema, { minutes, userName, address, reason, visibleReason, clientid, removeMessages }), { onSuccess: () => { diff --git a/webclient/src/websocket/commands/moderator/forceActivateUser.ts b/webclient/src/websocket/commands/moderator/forceActivateUser.ts index b31f8df31..99538a173 100644 --- a/webclient/src/websocket/commands/moderator/forceActivateUser.ts +++ b/webclient/src/websocket/commands/moderator/forceActivateUser.ts @@ -1,13 +1,12 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { - Command_ForceActivateUser_ext, Command_ForceActivateUserSchema, -} from 'generated/proto/moderator_commands_pb'; + import { ModeratorPersistence } from '../../persistence'; +import { Data } from '@app/types'; export function forceActivateUser(usernameToActivate: string, moderatorName: string): void { - const cmd = create(Command_ForceActivateUserSchema, { usernameToActivate, moderatorName }); - webClient.protobuf.sendModeratorCommand(Command_ForceActivateUser_ext, cmd, { + const cmd = create(Data.Command_ForceActivateUserSchema, { usernameToActivate, moderatorName }); + webClient.protobuf.sendModeratorCommand(Data.Command_ForceActivateUser_ext, cmd, { onSuccess: () => { ModeratorPersistence.forceActivateUser(usernameToActivate, moderatorName); }, diff --git a/webclient/src/websocket/commands/moderator/getAdminNotes.ts b/webclient/src/websocket/commands/moderator/getAdminNotes.ts index 9f554e536..f33ae4839 100644 --- a/webclient/src/websocket/commands/moderator/getAdminNotes.ts +++ b/webclient/src/websocket/commands/moderator/getAdminNotes.ts @@ -1,12 +1,12 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_GetAdminNotes_ext, Command_GetAdminNotesSchema } from 'generated/proto/moderator_commands_pb'; + import { ModeratorPersistence } from '../../persistence'; -import { Response_GetAdminNotes_ext } from 'generated/proto/response_get_admin_notes_pb'; +import { Data } from '@app/types'; export function getAdminNotes(userName: string): void { - webClient.protobuf.sendModeratorCommand(Command_GetAdminNotes_ext, create(Command_GetAdminNotesSchema, { userName }), { - responseExt: Response_GetAdminNotes_ext, + webClient.protobuf.sendModeratorCommand(Data.Command_GetAdminNotes_ext, create(Data.Command_GetAdminNotesSchema, { userName }), { + responseExt: Data.Response_GetAdminNotes_ext, onSuccess: (response) => { ModeratorPersistence.getAdminNotes(userName, response.notes); }, diff --git a/webclient/src/websocket/commands/moderator/getBanHistory.ts b/webclient/src/websocket/commands/moderator/getBanHistory.ts index c0fc9253d..8c296c216 100644 --- a/webclient/src/websocket/commands/moderator/getBanHistory.ts +++ b/webclient/src/websocket/commands/moderator/getBanHistory.ts @@ -1,12 +1,12 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_GetBanHistory_ext, Command_GetBanHistorySchema } from 'generated/proto/moderator_commands_pb'; + import { ModeratorPersistence } from '../../persistence'; -import { Response_BanHistory_ext } from 'generated/proto/response_ban_history_pb'; +import { Data } from '@app/types'; export function getBanHistory(userName: string): void { - webClient.protobuf.sendModeratorCommand(Command_GetBanHistory_ext, create(Command_GetBanHistorySchema, { userName }), { - responseExt: Response_BanHistory_ext, + webClient.protobuf.sendModeratorCommand(Data.Command_GetBanHistory_ext, create(Data.Command_GetBanHistorySchema, { userName }), { + responseExt: Data.Response_BanHistory_ext, onSuccess: (response) => { ModeratorPersistence.banHistory(userName, response.banList); }, diff --git a/webclient/src/websocket/commands/moderator/getWarnHistory.ts b/webclient/src/websocket/commands/moderator/getWarnHistory.ts index 4699b9333..c49a8d220 100644 --- a/webclient/src/websocket/commands/moderator/getWarnHistory.ts +++ b/webclient/src/websocket/commands/moderator/getWarnHistory.ts @@ -1,12 +1,12 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_GetWarnHistory_ext, Command_GetWarnHistorySchema } from 'generated/proto/moderator_commands_pb'; + import { ModeratorPersistence } from '../../persistence'; -import { Response_WarnHistory_ext } from 'generated/proto/response_warn_history_pb'; +import { Data } from '@app/types'; export function getWarnHistory(userName: string): void { - webClient.protobuf.sendModeratorCommand(Command_GetWarnHistory_ext, create(Command_GetWarnHistorySchema, { userName }), { - responseExt: Response_WarnHistory_ext, + webClient.protobuf.sendModeratorCommand(Data.Command_GetWarnHistory_ext, create(Data.Command_GetWarnHistorySchema, { userName }), { + responseExt: Data.Response_WarnHistory_ext, onSuccess: (response) => { ModeratorPersistence.warnHistory(userName, response.warnList); }, diff --git a/webclient/src/websocket/commands/moderator/getWarnList.ts b/webclient/src/websocket/commands/moderator/getWarnList.ts index 60c268f10..4ec9eef02 100644 --- a/webclient/src/websocket/commands/moderator/getWarnList.ts +++ b/webclient/src/websocket/commands/moderator/getWarnList.ts @@ -1,14 +1,18 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_GetWarnList_ext, Command_GetWarnListSchema } from 'generated/proto/moderator_commands_pb'; + import { ModeratorPersistence } from '../../persistence'; -import { Response_WarnList_ext } from 'generated/proto/response_warn_list_pb'; +import { Data } from '@app/types'; export function getWarnList(modName: string, userName: string, userClientid: string): void { - webClient.protobuf.sendModeratorCommand(Command_GetWarnList_ext, create(Command_GetWarnListSchema, { modName, userName, userClientid }), { - responseExt: Response_WarnList_ext, - onSuccess: (response) => { - ModeratorPersistence.warnListOptions([response]); - }, - }); + webClient.protobuf.sendModeratorCommand( + Data.Command_GetWarnList_ext, + create(Data.Command_GetWarnListSchema, { modName, userName, userClientid }), + { + responseExt: Data.Response_WarnList_ext, + onSuccess: (response) => { + ModeratorPersistence.warnListOptions([response]); + }, + } + ); } diff --git a/webclient/src/websocket/commands/moderator/grantReplayAccess.ts b/webclient/src/websocket/commands/moderator/grantReplayAccess.ts index 3826dd9d4..ebd59f4f8 100644 --- a/webclient/src/websocket/commands/moderator/grantReplayAccess.ts +++ b/webclient/src/websocket/commands/moderator/grantReplayAccess.ts @@ -1,14 +1,13 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { - Command_GrantReplayAccess_ext, Command_GrantReplayAccessSchema, -} from 'generated/proto/moderator_commands_pb'; + import { ModeratorPersistence } from '../../persistence'; +import { Data } from '@app/types'; export function grantReplayAccess(replayId: number, moderatorName: string): void { webClient.protobuf.sendModeratorCommand( - Command_GrantReplayAccess_ext, - create(Command_GrantReplayAccessSchema, { replayId, moderatorName }), + Data.Command_GrantReplayAccess_ext, + create(Data.Command_GrantReplayAccessSchema, { replayId, moderatorName }), { onSuccess: () => { ModeratorPersistence.grantReplayAccess(replayId, moderatorName); diff --git a/webclient/src/websocket/commands/moderator/moderatorCommands.spec.ts b/webclient/src/websocket/commands/moderator/moderatorCommands.spec.ts index 70a1bd9ff..e4be3d0c9 100644 --- a/webclient/src/websocket/commands/moderator/moderatorCommands.spec.ts +++ b/webclient/src/websocket/commands/moderator/moderatorCommands.spec.ts @@ -20,24 +20,9 @@ vi.mock('../../persistence', () => ({ import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers'; import webClient from '../../WebClient'; +import { Data } from '@app/types'; import { ModeratorPersistence } from '../../persistence'; -import { - Command_BanFromServer_ext, - Command_ForceActivateUser_ext, - Command_GetAdminNotes_ext, - Command_GetBanHistory_ext, - Command_GetWarnHistory_ext, - Command_GetWarnList_ext, - Command_GrantReplayAccess_ext, - Command_UpdateAdminNotes_ext, - Command_ViewLogHistory_ext, - Command_WarnUser_ext, -} from 'generated/proto/moderator_commands_pb'; -import { Response_GetAdminNotes_ext } from 'generated/proto/response_get_admin_notes_pb'; -import { Response_BanHistory_ext } from 'generated/proto/response_ban_history_pb'; -import { Response_WarnHistory_ext } from 'generated/proto/response_warn_history_pb'; -import { Response_WarnList_ext } from 'generated/proto/response_warn_list_pb'; -import { Response_ViewLogHistory_ext } from 'generated/proto/response_viewlog_history_pb'; + import { banFromServer } from './banFromServer'; import { forceActivateUser } from './forceActivateUser'; import { getAdminNotes } from './getAdminNotes'; @@ -48,7 +33,7 @@ import { grantReplayAccess } from './grantReplayAccess'; import { updateAdminNotes } from './updateAdminNotes'; import { viewLogHistory } from './viewLogHistory'; import { warnUser } from './warnUser'; - +import { create } from '@bufbuild/protobuf'; import { Mock } from 'vitest'; const { invokeOnSuccess } = makeCallbackHelpers( @@ -56,8 +41,6 @@ const { invokeOnSuccess } = makeCallbackHelpers( 2 ); -beforeEach(() => vi.clearAllMocks()); - // ---------------------------------------------------------------- // banFromServer // ---------------------------------------------------------------- @@ -66,7 +49,7 @@ describe('banFromServer', () => { it('calls sendModeratorCommand with Command_BanFromServer', () => { banFromServer(30, 'alice', '1.2.3.4', 'reason', 'visible', 'cid', 1); expect(webClient.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_BanFromServer_ext, + Data.Command_BanFromServer_ext, expect.objectContaining({ minutes: 30, userName: 'alice' }), expect.any(Object) ); @@ -87,7 +70,7 @@ describe('forceActivateUser', () => { it('calls sendModeratorCommand with Command_ForceActivateUser', () => { forceActivateUser('alice', 'mod1'); expect(webClient.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_ForceActivateUser_ext, expect.any(Object), expect.any(Object) + Data.Command_ForceActivateUser_ext, expect.any(Object), expect.any(Object) ); }); @@ -106,9 +89,9 @@ describe('getAdminNotes', () => { it('calls sendModeratorCommand with Command_GetAdminNotes', () => { getAdminNotes('alice'); expect(webClient.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_GetAdminNotes_ext, + Data.Command_GetAdminNotes_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_GetAdminNotes_ext }) + expect.objectContaining({ responseExt: Data.Response_GetAdminNotes_ext }) ); }); @@ -128,9 +111,9 @@ describe('getBanHistory', () => { it('calls sendModeratorCommand with Command_GetBanHistory', () => { getBanHistory('alice'); expect(webClient.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_GetBanHistory_ext, + Data.Command_GetBanHistory_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_BanHistory_ext }) + expect.objectContaining({ responseExt: Data.Response_BanHistory_ext }) ); }); @@ -150,9 +133,9 @@ describe('getWarnHistory', () => { it('calls sendModeratorCommand with Command_GetWarnHistory', () => { getWarnHistory('alice'); expect(webClient.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_GetWarnHistory_ext, + Data.Command_GetWarnHistory_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_WarnHistory_ext }) + expect.objectContaining({ responseExt: Data.Response_WarnHistory_ext }) ); }); @@ -172,9 +155,9 @@ describe('getWarnList', () => { it('calls sendModeratorCommand with Command_GetWarnList', () => { getWarnList('mod1', 'alice', 'US'); expect(webClient.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_GetWarnList_ext, + Data.Command_GetWarnList_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_WarnList_ext }) + expect.objectContaining({ responseExt: Data.Response_WarnList_ext }) ); }); @@ -194,7 +177,7 @@ describe('grantReplayAccess', () => { it('calls sendModeratorCommand with Command_GrantReplayAccess', () => { grantReplayAccess(10, 'mod1'); expect(webClient.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_GrantReplayAccess_ext, expect.any(Object), expect.any(Object) + Data.Command_GrantReplayAccess_ext, expect.any(Object), expect.any(Object) ); }); @@ -213,7 +196,7 @@ describe('updateAdminNotes', () => { it('calls sendModeratorCommand with Command_UpdateAdminNotes', () => { updateAdminNotes('alice', 'new notes'); expect(webClient.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_UpdateAdminNotes_ext, expect.any(Object), expect.any(Object) + Data.Command_UpdateAdminNotes_ext, expect.any(Object), expect.any(Object) ); }); @@ -230,16 +213,18 @@ describe('updateAdminNotes', () => { describe('viewLogHistory', () => { it('calls sendModeratorCommand with Command_ViewLogHistory', () => { - viewLogHistory({ dateRange: 7 } as any); + const filters = create(Data.Command_ViewLogHistorySchema, { dateRange: 7 }); + viewLogHistory(filters); expect(webClient.protobuf.sendModeratorCommand).toHaveBeenCalledWith( - Command_ViewLogHistory_ext, + Data.Command_ViewLogHistory_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_ViewLogHistory_ext }) + expect.objectContaining({ responseExt: Data.Response_ViewLogHistory_ext }) ); }); it('onSuccess calls ModeratorPersistence.viewLogs with logMessage', () => { - viewLogHistory({ dateRange: 7 } as any); + const filters = create(Data.Command_ViewLogHistorySchema, { dateRange: 7 }); + viewLogHistory(filters); const resp = { logMessage: ['log1'] }; invokeOnSuccess(resp, { responseCode: 0 }); expect(ModeratorPersistence.viewLogs).toHaveBeenCalledWith(['log1']); @@ -253,7 +238,7 @@ describe('warnUser', () => { it('calls sendModeratorCommand with Command_WarnUser', () => { warnUser('alice', 'bad behavior', 'cid'); - expect(webClient.protobuf.sendModeratorCommand).toHaveBeenCalledWith(Command_WarnUser_ext, expect.any(Object), expect.any(Object)); + expect(webClient.protobuf.sendModeratorCommand).toHaveBeenCalledWith(Data.Command_WarnUser_ext, expect.any(Object), expect.any(Object)); }); it('onSuccess calls ModeratorPersistence.warnUser', () => { diff --git a/webclient/src/websocket/commands/moderator/updateAdminNotes.ts b/webclient/src/websocket/commands/moderator/updateAdminNotes.ts index 448f58e23..f0a284fb3 100644 --- a/webclient/src/websocket/commands/moderator/updateAdminNotes.ts +++ b/webclient/src/websocket/commands/moderator/updateAdminNotes.ts @@ -1,14 +1,17 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { - Command_UpdateAdminNotes_ext, Command_UpdateAdminNotesSchema, -} from 'generated/proto/moderator_commands_pb'; + import { ModeratorPersistence } from '../../persistence'; +import { Data } from '@app/types'; export function updateAdminNotes(userName: string, notes: string): void { - webClient.protobuf.sendModeratorCommand(Command_UpdateAdminNotes_ext, create(Command_UpdateAdminNotesSchema, { userName, notes }), { - onSuccess: () => { - ModeratorPersistence.updateAdminNotes(userName, notes); - }, - }); + webClient.protobuf.sendModeratorCommand( + Data.Command_UpdateAdminNotes_ext, + create(Data.Command_UpdateAdminNotesSchema, { userName, notes }), + { + onSuccess: () => { + ModeratorPersistence.updateAdminNotes(userName, notes); + }, + } + ); } diff --git a/webclient/src/websocket/commands/moderator/viewLogHistory.ts b/webclient/src/websocket/commands/moderator/viewLogHistory.ts index 264a535b2..f09f2fe0d 100644 --- a/webclient/src/websocket/commands/moderator/viewLogHistory.ts +++ b/webclient/src/websocket/commands/moderator/viewLogHistory.ts @@ -1,13 +1,13 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_ViewLogHistory_ext, Command_ViewLogHistorySchema } from 'generated/proto/moderator_commands_pb'; -import { ModeratorPersistence } from '../../persistence'; -import { Response_ViewLogHistory_ext } from 'generated/proto/response_viewlog_history_pb'; -import { LogFilters } from 'types'; -export function viewLogHistory(filters: LogFilters): void { - webClient.protobuf.sendModeratorCommand(Command_ViewLogHistory_ext, create(Command_ViewLogHistorySchema, filters), { - responseExt: Response_ViewLogHistory_ext, +import { ModeratorPersistence } from '../../persistence'; + +import { Data } from '@app/types'; + +export function viewLogHistory(filters: Data.ViewLogHistoryParams): void { + webClient.protobuf.sendModeratorCommand(Data.Command_ViewLogHistory_ext, create(Data.Command_ViewLogHistorySchema, filters), { + responseExt: Data.Response_ViewLogHistory_ext, onSuccess: (response) => { ModeratorPersistence.viewLogs(response.logMessage); }, diff --git a/webclient/src/websocket/commands/moderator/warnUser.ts b/webclient/src/websocket/commands/moderator/warnUser.ts index dfe2f3211..4a441a074 100644 --- a/webclient/src/websocket/commands/moderator/warnUser.ts +++ b/webclient/src/websocket/commands/moderator/warnUser.ts @@ -1,11 +1,12 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_WarnUser_ext, Command_WarnUserSchema } from 'generated/proto/moderator_commands_pb'; + import { ModeratorPersistence } from '../../persistence'; +import { Data } from '@app/types'; export function warnUser(userName: string, reason: string, clientid?: string, removeMessages?: number): void { - const cmd = create(Command_WarnUserSchema, { userName, reason, clientid, removeMessages }); - webClient.protobuf.sendModeratorCommand(Command_WarnUser_ext, cmd, { + const cmd = create(Data.Command_WarnUserSchema, { userName, reason, clientid, removeMessages }); + webClient.protobuf.sendModeratorCommand(Data.Command_WarnUser_ext, cmd, { onSuccess: () => { ModeratorPersistence.warnUser(userName); }, diff --git a/webclient/src/websocket/commands/room/createGame.ts b/webclient/src/websocket/commands/room/createGame.ts index be1b5b401..a531614fe 100644 --- a/webclient/src/websocket/commands/room/createGame.ts +++ b/webclient/src/websocket/commands/room/createGame.ts @@ -1,11 +1,11 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_CreateGame_ext, Command_CreateGameSchema } from 'generated/proto/room_commands_pb'; -import { RoomPersistence } from '../../persistence'; -import { GameConfig } from 'types'; -export function createGame(roomId: number, gameConfig: GameConfig): void { - webClient.protobuf.sendRoomCommand(roomId, Command_CreateGame_ext, create(Command_CreateGameSchema, gameConfig), { +import { RoomPersistence } from '../../persistence'; +import { Data } from '@app/types'; + +export function createGame(roomId: number, gameConfig: Data.CreateGameParams): void { + webClient.protobuf.sendRoomCommand(roomId, Data.Command_CreateGame_ext, create(Data.Command_CreateGameSchema, gameConfig), { onSuccess: () => { RoomPersistence.gameCreated(roomId); }, diff --git a/webclient/src/websocket/commands/room/joinGame.ts b/webclient/src/websocket/commands/room/joinGame.ts index 9975eccef..97364e0d1 100644 --- a/webclient/src/websocket/commands/room/joinGame.ts +++ b/webclient/src/websocket/commands/room/joinGame.ts @@ -1,11 +1,11 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_JoinGame_ext, Command_JoinGameSchema } from 'generated/proto/room_commands_pb'; -import { RoomPersistence } from '../../persistence'; -import { JoinGameParams } from 'types'; -export function joinGame(roomId: number, joinGameParams: JoinGameParams): void { - webClient.protobuf.sendRoomCommand(roomId, Command_JoinGame_ext, create(Command_JoinGameSchema, joinGameParams), { +import { RoomPersistence } from '../../persistence'; +import { Data } from '@app/types'; + +export function joinGame(roomId: number, joinGameParams: Data.JoinGameParams): void { + webClient.protobuf.sendRoomCommand(roomId, Data.Command_JoinGame_ext, create(Data.Command_JoinGameSchema, joinGameParams), { onSuccess: () => { RoomPersistence.joinedGame(roomId, joinGameParams.gameId); }, diff --git a/webclient/src/websocket/commands/room/leaveRoom.ts b/webclient/src/websocket/commands/room/leaveRoom.ts index 67303f510..7203239f9 100644 --- a/webclient/src/websocket/commands/room/leaveRoom.ts +++ b/webclient/src/websocket/commands/room/leaveRoom.ts @@ -1,10 +1,11 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_LeaveRoom_ext, Command_LeaveRoomSchema } from 'generated/proto/room_commands_pb'; + import { RoomPersistence } from '../../persistence'; +import { Data } from '@app/types'; export function leaveRoom(roomId: number): void { - webClient.protobuf.sendRoomCommand(roomId, Command_LeaveRoom_ext, create(Command_LeaveRoomSchema), { + webClient.protobuf.sendRoomCommand(roomId, Data.Command_LeaveRoom_ext, create(Data.Command_LeaveRoomSchema), { onSuccess: () => { RoomPersistence.leaveRoom(roomId); }, diff --git a/webclient/src/websocket/commands/room/roomCommands.spec.ts b/webclient/src/websocket/commands/room/roomCommands.spec.ts index 0916eb233..be55fadc0 100644 --- a/webclient/src/websocket/commands/room/roomCommands.spec.ts +++ b/webclient/src/websocket/commands/room/roomCommands.spec.ts @@ -13,13 +13,14 @@ vi.mock('../../persistence', () => ({ import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers'; import webClient from '../../WebClient'; +import { Data } from '@app/types'; import { RoomPersistence } from '../../persistence'; -import { Command_CreateGame_ext, Command_JoinGame_ext, Command_LeaveRoom_ext, Command_RoomSay_ext } from 'generated/proto/room_commands_pb'; + import { createGame } from './createGame'; import { joinGame } from './joinGame'; import { leaveRoom } from './leaveRoom'; import { roomSay } from './roomSay'; - +import { create } from '@bufbuild/protobuf'; import { Mock } from 'vitest'; const { invokeOnSuccess } = makeCallbackHelpers( @@ -28,22 +29,20 @@ const { invokeOnSuccess } = makeCallbackHelpers( 3 ); -beforeEach(() => vi.clearAllMocks()); - // ---------------------------------------------------------------- // createGame // ---------------------------------------------------------------- describe('createGame', () => { it('calls sendRoomCommand with Command_CreateGame', () => { - createGame(5, { maxPlayers: 4 } as any); + createGame(5, create(Data.Command_CreateGameSchema, { maxPlayers: 4 })); expect(webClient.protobuf.sendRoomCommand).toHaveBeenCalledWith( - 5, Command_CreateGame_ext, expect.objectContaining({ maxPlayers: 4 }), expect.any(Object) + 5, Data.Command_CreateGame_ext, expect.objectContaining({ maxPlayers: 4 }), expect.any(Object) ); }); it('onSuccess calls RoomPersistence.gameCreated with roomId', () => { - createGame(5, {} as any); + createGame(5, create(Data.Command_CreateGameSchema, {})); invokeOnSuccess(); expect(RoomPersistence.gameCreated).toHaveBeenCalledWith(5); }); @@ -55,14 +54,14 @@ describe('createGame', () => { describe('joinGame', () => { it('calls sendRoomCommand with Command_JoinGame', () => { - joinGame(7, { gameId: 42, password: '' } as any); + joinGame(7, create(Data.Command_JoinGameSchema, { gameId: 42, password: '' })); expect(webClient.protobuf.sendRoomCommand).toHaveBeenCalledWith( - 7, Command_JoinGame_ext, expect.objectContaining({ gameId: 42, password: '' }), expect.any(Object) + 7, Data.Command_JoinGame_ext, expect.objectContaining({ gameId: 42, password: '' }), expect.any(Object) ); }); it('onSuccess calls RoomPersistence.joinedGame with roomId and gameId', () => { - joinGame(7, { gameId: 42 } as any); + joinGame(7, create(Data.Command_JoinGameSchema, { gameId: 42 })); invokeOnSuccess(); expect(RoomPersistence.joinedGame).toHaveBeenCalledWith(7, 42); }); @@ -75,7 +74,7 @@ describe('leaveRoom', () => { it('calls sendRoomCommand with Command_LeaveRoom', () => { leaveRoom(3); - expect(webClient.protobuf.sendRoomCommand).toHaveBeenCalledWith(3, Command_LeaveRoom_ext, expect.any(Object), expect.any(Object)); + expect(webClient.protobuf.sendRoomCommand).toHaveBeenCalledWith(3, Data.Command_LeaveRoom_ext, expect.any(Object), expect.any(Object)); }); it('onSuccess calls RoomPersistence.leaveRoom with roomId', () => { @@ -94,7 +93,7 @@ describe('roomSay', () => { roomSay(2, ' hello '); expect(webClient.protobuf.sendRoomCommand).toHaveBeenCalledWith( 2, - Command_RoomSay_ext, + Data.Command_RoomSay_ext, expect.objectContaining({ message: 'hello' }) ); }); diff --git a/webclient/src/websocket/commands/room/roomSay.ts b/webclient/src/websocket/commands/room/roomSay.ts index d847dc527..74c44ada8 100644 --- a/webclient/src/websocket/commands/room/roomSay.ts +++ b/webclient/src/websocket/commands/room/roomSay.ts @@ -1,6 +1,6 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_RoomSay_ext, Command_RoomSaySchema } from 'generated/proto/room_commands_pb'; +import { Data } from '@app/types'; export function roomSay(roomId: number, message: string): void { const trimmed = message.trim(); @@ -9,5 +9,5 @@ export function roomSay(roomId: number, message: string): void { return; } - webClient.protobuf.sendRoomCommand(roomId, Command_RoomSay_ext, create(Command_RoomSaySchema, { message: trimmed })); + webClient.protobuf.sendRoomCommand(roomId, Data.Command_RoomSay_ext, create(Data.Command_RoomSaySchema, { message: trimmed })); } diff --git a/webclient/src/websocket/commands/session/accountEdit.ts b/webclient/src/websocket/commands/session/accountEdit.ts index ad77b4dfb..2dd77f737 100644 --- a/webclient/src/websocket/commands/session/accountEdit.ts +++ b/webclient/src/websocket/commands/session/accountEdit.ts @@ -1,11 +1,12 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_AccountEdit_ext, Command_AccountEditSchema } from 'generated/proto/session_commands_pb'; + import { SessionPersistence } from '../../persistence'; +import { Data } from '@app/types'; export function accountEdit(passwordCheck: string, realName?: string, email?: string, country?: string): void { - const cmd = create(Command_AccountEditSchema, { passwordCheck, realName, email, country }); - webClient.protobuf.sendSessionCommand(Command_AccountEdit_ext, cmd, { + const cmd = create(Data.Command_AccountEditSchema, { passwordCheck, realName, email, country }); + webClient.protobuf.sendSessionCommand(Data.Command_AccountEdit_ext, cmd, { onSuccess: () => { SessionPersistence.accountEditChanged(realName, email, country); }, diff --git a/webclient/src/websocket/commands/session/accountImage.ts b/webclient/src/websocket/commands/session/accountImage.ts index da603055f..36e7a78b4 100644 --- a/webclient/src/websocket/commands/session/accountImage.ts +++ b/webclient/src/websocket/commands/session/accountImage.ts @@ -1,10 +1,11 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_AccountImage_ext, Command_AccountImageSchema } from 'generated/proto/session_commands_pb'; + import { SessionPersistence } from '../../persistence'; +import { Data } from '@app/types'; export function accountImage(image: Uint8Array): void { - webClient.protobuf.sendSessionCommand(Command_AccountImage_ext, create(Command_AccountImageSchema, { image }), { + webClient.protobuf.sendSessionCommand(Data.Command_AccountImage_ext, create(Data.Command_AccountImageSchema, { image }), { onSuccess: () => { SessionPersistence.accountImageChanged(image); }, diff --git a/webclient/src/websocket/commands/session/accountPassword.ts b/webclient/src/websocket/commands/session/accountPassword.ts index 5cc561421..947dc4ad2 100644 --- a/webclient/src/websocket/commands/session/accountPassword.ts +++ b/webclient/src/websocket/commands/session/accountPassword.ts @@ -1,11 +1,12 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_AccountPassword_ext, Command_AccountPasswordSchema } from 'generated/proto/session_commands_pb'; + import { SessionPersistence } from '../../persistence'; +import { Data } from '@app/types'; export function accountPassword(oldPassword: string, newPassword: string, hashedNewPassword: string): void { - const cmd = create(Command_AccountPasswordSchema, { oldPassword, newPassword, hashedNewPassword }); - webClient.protobuf.sendSessionCommand(Command_AccountPassword_ext, cmd, { + const cmd = create(Data.Command_AccountPasswordSchema, { oldPassword, newPassword, hashedNewPassword }); + webClient.protobuf.sendSessionCommand(Data.Command_AccountPassword_ext, cmd, { onSuccess: () => { SessionPersistence.accountPasswordChange(); }, diff --git a/webclient/src/websocket/commands/session/activate.ts b/webclient/src/websocket/commands/session/activate.ts index 49614ab37..45faf1303 100644 --- a/webclient/src/websocket/commands/session/activate.ts +++ b/webclient/src/websocket/commands/session/activate.ts @@ -1,31 +1,34 @@ -import { AccountActivationParams } from 'store'; -import { StatusEnum, WebSocketConnectOptions } from 'types'; +import { App, Enriched, Data } from '@app/types'; import { create } from '@bufbuild/protobuf'; import { CLIENT_CONFIG } from '../../config'; import webClient from '../../WebClient'; -import { Command_Activate_ext, Command_ActivateSchema } from 'generated/proto/session_commands_pb'; + import { SessionPersistence } from '../../persistence'; -import { Response_ResponseCode } from 'generated/proto/response_pb'; import { disconnect, login, updateStatus } from './'; -export function activate(options: WebSocketConnectOptions, password?: string, passwordSalt?: string): void { - const { userName, token } = options as unknown as AccountActivationParams; +export function activate(options: Omit, password?: string, passwordSalt?: string): void { + const { userName, token } = options; - webClient.protobuf.sendSessionCommand(Command_Activate_ext, create(Command_ActivateSchema, { + webClient.protobuf.sendSessionCommand(Data.Command_Activate_ext, create(Data.Command_ActivateSchema, { ...CLIENT_CONFIG, userName, token, }), { onResponseCode: { - [Response_ResponseCode.RespActivationAccepted]: () => { + [Data.Response_ResponseCode.RespActivationAccepted]: () => { SessionPersistence.accountActivationSuccess(); - login(options, password, passwordSalt); + login({ + host: options.host, + port: options.port, + userName: options.userName, + reason: App.WebSocketConnectReason.LOGIN, + }, password, passwordSalt); }, }, onError: () => { - updateStatus(StatusEnum.DISCONNECTED, 'Account Activation Failed'); + updateStatus(App.StatusEnum.DISCONNECTED, 'Account Activation Failed'); disconnect(); SessionPersistence.accountActivationFailed(); }, diff --git a/webclient/src/websocket/commands/session/addToList.ts b/webclient/src/websocket/commands/session/addToList.ts index 74bdfa23c..dac79b91d 100644 --- a/webclient/src/websocket/commands/session/addToList.ts +++ b/webclient/src/websocket/commands/session/addToList.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_AddToList_ext, Command_AddToListSchema } from 'generated/proto/session_commands_pb'; + import { SessionPersistence } from '../../persistence'; +import { Data } from '@app/types'; export function addToBuddyList(userName: string): void { addToList('buddy', userName); @@ -12,7 +13,7 @@ export function addToIgnoreList(userName: string): void { } export function addToList(list: string, userName: string): void { - webClient.protobuf.sendSessionCommand(Command_AddToList_ext, create(Command_AddToListSchema, { list, userName }), { + webClient.protobuf.sendSessionCommand(Data.Command_AddToList_ext, create(Data.Command_AddToListSchema, { list, userName }), { onSuccess: () => { SessionPersistence.addToList(list, userName); }, diff --git a/webclient/src/websocket/commands/session/connect.ts b/webclient/src/websocket/commands/session/connect.ts index 5b660fab1..b1b7e2793 100644 --- a/webclient/src/websocket/commands/session/connect.ts +++ b/webclient/src/websocket/commands/session/connect.ts @@ -1,24 +1,25 @@ -import { StatusEnum, WebSocketConnectOptions, WebSocketConnectReason } from 'types'; +import { App, Enriched } from '@app/types'; import webClient from '../../WebClient'; import { updateStatus } from './'; -export function connect(options: WebSocketConnectOptions, reason: WebSocketConnectReason): void { - switch (reason) { - case WebSocketConnectReason.LOGIN: - case WebSocketConnectReason.REGISTER: - case WebSocketConnectReason.ACTIVATE_ACCOUNT: - case WebSocketConnectReason.PASSWORD_RESET_REQUEST: - case WebSocketConnectReason.PASSWORD_RESET_CHALLENGE: - case WebSocketConnectReason.PASSWORD_RESET: - updateStatus(StatusEnum.CONNECTING, 'Connecting...'); - break; - case WebSocketConnectReason.TEST_CONNECTION: - webClient.testConnect({ ...options }); +export function connect(options: Enriched.WebSocketConnectOptions): void { + switch (options.reason) { + case App.WebSocketConnectReason.LOGIN: + case App.WebSocketConnectReason.REGISTER: + case App.WebSocketConnectReason.ACTIVATE_ACCOUNT: + case App.WebSocketConnectReason.PASSWORD_RESET_REQUEST: + case App.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE: + case App.WebSocketConnectReason.PASSWORD_RESET: + updateStatus(App.StatusEnum.CONNECTING, 'Connecting...'); + webClient.connect(options); return; - default: - updateStatus(StatusEnum.DISCONNECTED, 'Unknown Connection Attempt: ' + reason); + case App.WebSocketConnectReason.TEST_CONNECTION: + webClient.testConnect(options); return; + default: { + const { reason } = options as Enriched.WebSocketConnectOptions; + updateStatus(App.StatusEnum.DISCONNECTED, `Unknown Connection Attempt: ${reason}`); + return; + } } - - webClient.connect({ ...options, reason }); } diff --git a/webclient/src/websocket/commands/session/deckDel.ts b/webclient/src/websocket/commands/session/deckDel.ts index bf0bd2672..d75cbdf8a 100644 --- a/webclient/src/websocket/commands/session/deckDel.ts +++ b/webclient/src/websocket/commands/session/deckDel.ts @@ -1,10 +1,11 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_DeckDelSchema, Command_DeckDel_ext } from 'generated/proto/command_deck_del_pb'; + import { SessionPersistence } from '../../persistence'; +import { Data } from '@app/types'; export function deckDel(deckId: number): void { - webClient.protobuf.sendSessionCommand(Command_DeckDel_ext, create(Command_DeckDelSchema, { deckId }), { + webClient.protobuf.sendSessionCommand(Data.Command_DeckDel_ext, create(Data.Command_DeckDelSchema, { deckId }), { onSuccess: () => { SessionPersistence.deleteServerDeck(deckId); }, diff --git a/webclient/src/websocket/commands/session/deckDelDir.ts b/webclient/src/websocket/commands/session/deckDelDir.ts index 7c442b575..30f6b94bc 100644 --- a/webclient/src/websocket/commands/session/deckDelDir.ts +++ b/webclient/src/websocket/commands/session/deckDelDir.ts @@ -1,10 +1,11 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_DeckDelDirSchema, Command_DeckDelDir_ext } from 'generated/proto/command_deck_del_dir_pb'; + import { SessionPersistence } from '../../persistence'; +import { Data } from '@app/types'; export function deckDelDir(path: string): void { - webClient.protobuf.sendSessionCommand(Command_DeckDelDir_ext, create(Command_DeckDelDirSchema, { path }), { + webClient.protobuf.sendSessionCommand(Data.Command_DeckDelDir_ext, create(Data.Command_DeckDelDirSchema, { path }), { onSuccess: () => { SessionPersistence.deleteServerDeckDir(path); }, diff --git a/webclient/src/websocket/commands/session/deckList.ts b/webclient/src/websocket/commands/session/deckList.ts index 71f611a26..12a240232 100644 --- a/webclient/src/websocket/commands/session/deckList.ts +++ b/webclient/src/websocket/commands/session/deckList.ts @@ -1,12 +1,12 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_DeckListSchema, Command_DeckList_ext } from 'generated/proto/command_deck_list_pb'; + import { SessionPersistence } from '../../persistence'; -import { Response_DeckList_ext } from 'generated/proto/response_deck_list_pb'; +import { Data } from '@app/types'; export function deckList(): void { - webClient.protobuf.sendSessionCommand(Command_DeckList_ext, create(Command_DeckListSchema), { - responseExt: Response_DeckList_ext, + webClient.protobuf.sendSessionCommand(Data.Command_DeckList_ext, create(Data.Command_DeckListSchema), { + responseExt: Data.Response_DeckList_ext, onSuccess: (response) => { if (response.root) { SessionPersistence.updateServerDecks(response); diff --git a/webclient/src/websocket/commands/session/deckNewDir.ts b/webclient/src/websocket/commands/session/deckNewDir.ts index d208bf0f4..566baeb15 100644 --- a/webclient/src/websocket/commands/session/deckNewDir.ts +++ b/webclient/src/websocket/commands/session/deckNewDir.ts @@ -1,10 +1,11 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_DeckNewDirSchema, Command_DeckNewDir_ext } from 'generated/proto/command_deck_new_dir_pb'; + import { SessionPersistence } from '../../persistence'; +import { Data } from '@app/types'; export function deckNewDir(path: string, dirName: string): void { - webClient.protobuf.sendSessionCommand(Command_DeckNewDir_ext, create(Command_DeckNewDirSchema, { path, dirName }), { + webClient.protobuf.sendSessionCommand(Data.Command_DeckNewDir_ext, create(Data.Command_DeckNewDirSchema, { path, dirName }), { onSuccess: () => { SessionPersistence.createServerDeckDir(path, dirName); }, diff --git a/webclient/src/websocket/commands/session/deckUpload.ts b/webclient/src/websocket/commands/session/deckUpload.ts index a13596329..84f8b64ef 100644 --- a/webclient/src/websocket/commands/session/deckUpload.ts +++ b/webclient/src/websocket/commands/session/deckUpload.ts @@ -1,12 +1,12 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_DeckUploadSchema, Command_DeckUpload_ext } from 'generated/proto/command_deck_upload_pb'; + import { SessionPersistence } from '../../persistence'; -import { Response_DeckUpload_ext } from 'generated/proto/response_deck_upload_pb'; +import { Data } from '@app/types'; export function deckUpload(path: string, deckId: number, deckList: string): void { - webClient.protobuf.sendSessionCommand(Command_DeckUpload_ext, create(Command_DeckUploadSchema, { path, deckId, deckList }), { - responseExt: Response_DeckUpload_ext, + webClient.protobuf.sendSessionCommand(Data.Command_DeckUpload_ext, create(Data.Command_DeckUploadSchema, { path, deckId, deckList }), { + responseExt: Data.Response_DeckUpload_ext, onSuccess: (response) => { if (response.newFile) { SessionPersistence.uploadServerDeck(path, response.newFile); diff --git a/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts b/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts index 98cfb25ac..8b6842414 100644 --- a/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts +++ b/webclient/src/websocket/commands/session/forgotPasswordChallenge.ts @@ -1,30 +1,27 @@ -import { ForgotPasswordChallengeParams } from 'store'; -import { StatusEnum, WebSocketConnectOptions } from 'types'; +import { App, Enriched, Data } from '@app/types'; import { create } from '@bufbuild/protobuf'; import { CLIENT_CONFIG } from '../../config'; import webClient from '../../WebClient'; -import { - Command_ForgotPasswordChallenge_ext, Command_ForgotPasswordChallengeSchema, -} from 'generated/proto/session_commands_pb'; + import { SessionPersistence } from '../../persistence'; import { disconnect, updateStatus } from './'; -export function forgotPasswordChallenge(options: WebSocketConnectOptions): void { - const { userName, email } = options as unknown as ForgotPasswordChallengeParams; +export function forgotPasswordChallenge(options: Enriched.PasswordResetChallengeConnectOptions): void { + const { userName, email } = options; - webClient.protobuf.sendSessionCommand(Command_ForgotPasswordChallenge_ext, create(Command_ForgotPasswordChallengeSchema, { + webClient.protobuf.sendSessionCommand(Data.Command_ForgotPasswordChallenge_ext, create(Data.Command_ForgotPasswordChallengeSchema, { ...CLIENT_CONFIG, userName, email, }), { onSuccess: () => { - updateStatus(StatusEnum.DISCONNECTED, null); + updateStatus(App.StatusEnum.DISCONNECTED, null); SessionPersistence.resetPassword(); disconnect(); }, onError: () => { - updateStatus(StatusEnum.DISCONNECTED, null); + updateStatus(App.StatusEnum.DISCONNECTED, null); SessionPersistence.resetPasswordFailed(); disconnect(); }, diff --git a/webclient/src/websocket/commands/session/forgotPasswordRequest.ts b/webclient/src/websocket/commands/session/forgotPasswordRequest.ts index 6760b3908..06f12390f 100644 --- a/webclient/src/websocket/commands/session/forgotPasswordRequest.ts +++ b/webclient/src/websocket/commands/session/forgotPasswordRequest.ts @@ -1,37 +1,33 @@ -import { ForgotPasswordParams } from 'store'; -import { StatusEnum, WebSocketConnectOptions } from 'types'; +import { App, Enriched, Data } from '@app/types'; import { create } from '@bufbuild/protobuf'; import { CLIENT_CONFIG } from '../../config'; import webClient from '../../WebClient'; -import { - Command_ForgotPasswordRequest_ext, Command_ForgotPasswordRequestSchema, -} from 'generated/proto/session_commands_pb'; + import { SessionPersistence } from '../../persistence'; -import { Response_ForgotPasswordRequest_ext } from 'generated/proto/response_forgotpasswordrequest_pb'; import { disconnect, updateStatus } from './'; -export function forgotPasswordRequest(options: WebSocketConnectOptions): void { - const { userName } = options as unknown as ForgotPasswordParams; +export function forgotPasswordRequest(options: Enriched.PasswordResetRequestConnectOptions): void { + const { userName } = options; - webClient.protobuf.sendSessionCommand(Command_ForgotPasswordRequest_ext, create(Command_ForgotPasswordRequestSchema, { + webClient.protobuf.sendSessionCommand(Data.Command_ForgotPasswordRequest_ext, create(Data.Command_ForgotPasswordRequestSchema, { ...CLIENT_CONFIG, userName, }), { - responseExt: Response_ForgotPasswordRequest_ext, + responseExt: Data.Response_ForgotPasswordRequest_ext, onSuccess: (resp) => { if (resp?.challengeEmail) { - updateStatus(StatusEnum.DISCONNECTED, null); + updateStatus(App.StatusEnum.DISCONNECTED, null); SessionPersistence.resetPasswordChallenge(); } else { - updateStatus(StatusEnum.DISCONNECTED, null); + updateStatus(App.StatusEnum.DISCONNECTED, null); SessionPersistence.resetPassword(); } disconnect(); }, onError: () => { - updateStatus(StatusEnum.DISCONNECTED, null); + updateStatus(App.StatusEnum.DISCONNECTED, null); SessionPersistence.resetPasswordFailed(); disconnect(); }, diff --git a/webclient/src/websocket/commands/session/forgotPasswordReset.ts b/webclient/src/websocket/commands/session/forgotPasswordReset.ts index 90625fd5c..a947d5e7f 100644 --- a/webclient/src/websocket/commands/session/forgotPasswordReset.ts +++ b/webclient/src/websocket/commands/session/forgotPasswordReset.ts @@ -1,22 +1,23 @@ -import { ForgotPasswordResetParams } from 'store'; -import { StatusEnum, WebSocketConnectOptions } from 'types'; +import { App, Enriched, Data } from '@app/types'; import { create } from '@bufbuild/protobuf'; import type { MessageInitShape } from '@bufbuild/protobuf'; import { CLIENT_CONFIG } from '../../config'; import webClient from '../../WebClient'; -import { - Command_ForgotPasswordReset_ext, Command_ForgotPasswordResetSchema, -} from 'generated/proto/session_commands_pb'; + import { SessionPersistence } from '../../persistence'; import { hashPassword } from '../../utils'; import { disconnect, updateStatus } from '.'; -export function forgotPasswordReset(options: WebSocketConnectOptions, newPassword?: string, passwordSalt?: string): void { - const { userName, token } = options as unknown as ForgotPasswordResetParams; +export function forgotPasswordReset( + options: Omit, + newPassword?: string, + passwordSalt?: string +): void { + const { userName, token } = options; - const params: MessageInitShape = { + const params: MessageInitShape = { ...CLIENT_CONFIG, userName, token, @@ -25,14 +26,14 @@ export function forgotPasswordReset(options: WebSocketConnectOptions, newPasswor : { newPassword }), }; - webClient.protobuf.sendSessionCommand(Command_ForgotPasswordReset_ext, create(Command_ForgotPasswordResetSchema, params), { + webClient.protobuf.sendSessionCommand(Data.Command_ForgotPasswordReset_ext, create(Data.Command_ForgotPasswordResetSchema, params), { onSuccess: () => { - updateStatus(StatusEnum.DISCONNECTED, null); + updateStatus(App.StatusEnum.DISCONNECTED, null); SessionPersistence.resetPasswordSuccess(); disconnect(); }, onError: () => { - updateStatus(StatusEnum.DISCONNECTED, null); + updateStatus(App.StatusEnum.DISCONNECTED, null); SessionPersistence.resetPasswordFailed(); disconnect(); }, diff --git a/webclient/src/websocket/commands/session/getGamesOfUser.ts b/webclient/src/websocket/commands/session/getGamesOfUser.ts index 184a31494..437e04425 100644 --- a/webclient/src/websocket/commands/session/getGamesOfUser.ts +++ b/webclient/src/websocket/commands/session/getGamesOfUser.ts @@ -1,12 +1,12 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_GetGamesOfUser_ext, Command_GetGamesOfUserSchema } from 'generated/proto/session_commands_pb'; + import { SessionPersistence } from '../../persistence'; -import { Response_GetGamesOfUser_ext } from 'generated/proto/response_get_games_of_user_pb'; +import { Data } from '@app/types'; export function getGamesOfUser(userName: string): void { - webClient.protobuf.sendSessionCommand(Command_GetGamesOfUser_ext, create(Command_GetGamesOfUserSchema, { userName }), { - responseExt: Response_GetGamesOfUser_ext, + webClient.protobuf.sendSessionCommand(Data.Command_GetGamesOfUser_ext, create(Data.Command_GetGamesOfUserSchema, { userName }), { + responseExt: Data.Response_GetGamesOfUser_ext, onSuccess: (response) => { SessionPersistence.getGamesOfUser(userName, response); }, diff --git a/webclient/src/websocket/commands/session/getUserInfo.ts b/webclient/src/websocket/commands/session/getUserInfo.ts index cf1bf0f4c..88a2a5c7e 100644 --- a/webclient/src/websocket/commands/session/getUserInfo.ts +++ b/webclient/src/websocket/commands/session/getUserInfo.ts @@ -1,12 +1,12 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_GetUserInfo_ext, Command_GetUserInfoSchema } from 'generated/proto/session_commands_pb'; + import { SessionPersistence } from '../../persistence'; -import { Response_GetUserInfo_ext } from 'generated/proto/response_get_user_info_pb'; +import { Data } from '@app/types'; export function getUserInfo(userName: string): void { - webClient.protobuf.sendSessionCommand(Command_GetUserInfo_ext, create(Command_GetUserInfoSchema, { userName }), { - responseExt: Response_GetUserInfo_ext, + webClient.protobuf.sendSessionCommand(Data.Command_GetUserInfo_ext, create(Data.Command_GetUserInfoSchema, { userName }), { + responseExt: Data.Response_GetUserInfo_ext, onSuccess: (response) => { SessionPersistence.getUserInfo(response.userInfo); }, diff --git a/webclient/src/websocket/commands/session/joinRoom.ts b/webclient/src/websocket/commands/session/joinRoom.ts index 267a7e07b..73c9590eb 100644 --- a/webclient/src/websocket/commands/session/joinRoom.ts +++ b/webclient/src/websocket/commands/session/joinRoom.ts @@ -1,12 +1,12 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_JoinRoom_ext, Command_JoinRoomSchema } from 'generated/proto/session_commands_pb'; + import { RoomPersistence } from '../../persistence'; -import { Response_JoinRoom_ext } from 'generated/proto/response_join_room_pb'; +import { Data } from '@app/types'; export function joinRoom(roomId: number): void { - webClient.protobuf.sendSessionCommand(Command_JoinRoom_ext, create(Command_JoinRoomSchema, { roomId }), { - responseExt: Response_JoinRoom_ext, + webClient.protobuf.sendSessionCommand(Data.Command_JoinRoom_ext, create(Data.Command_JoinRoomSchema, { roomId }), { + responseExt: Data.Response_JoinRoom_ext, onSuccess: (response) => { if (response.roomInfo) { RoomPersistence.joinRoom(response.roomInfo); diff --git a/webclient/src/websocket/commands/session/listRooms.ts b/webclient/src/websocket/commands/session/listRooms.ts index efaeab483..4e54f0e65 100644 --- a/webclient/src/websocket/commands/session/listRooms.ts +++ b/webclient/src/websocket/commands/session/listRooms.ts @@ -1,7 +1,7 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_ListRooms_ext, Command_ListRoomsSchema } from 'generated/proto/session_commands_pb'; +import { Data } from '@app/types'; export function listRooms(): void { - webClient.protobuf.sendSessionCommand(Command_ListRooms_ext, create(Command_ListRoomsSchema)); + webClient.protobuf.sendSessionCommand(Data.Command_ListRooms_ext, create(Data.Command_ListRoomsSchema)); } diff --git a/webclient/src/websocket/commands/session/listUsers.ts b/webclient/src/websocket/commands/session/listUsers.ts index 50f831234..519168875 100644 --- a/webclient/src/websocket/commands/session/listUsers.ts +++ b/webclient/src/websocket/commands/session/listUsers.ts @@ -1,12 +1,12 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_ListUsers_ext, Command_ListUsersSchema } from 'generated/proto/session_commands_pb'; + import { SessionPersistence } from '../../persistence'; -import { Response_ListUsers_ext } from 'generated/proto/response_list_users_pb'; +import { Data } from '@app/types'; export function listUsers(): void { - webClient.protobuf.sendSessionCommand(Command_ListUsers_ext, create(Command_ListUsersSchema), { - responseExt: Response_ListUsers_ext, + webClient.protobuf.sendSessionCommand(Data.Command_ListUsers_ext, create(Data.Command_ListUsersSchema), { + responseExt: Data.Response_ListUsers_ext, onSuccess: (response) => { SessionPersistence.updateUsers(response.userList); }, diff --git a/webclient/src/websocket/commands/session/login.ts b/webclient/src/websocket/commands/session/login.ts index 3d93ec0ec..57332a5a6 100644 --- a/webclient/src/websocket/commands/session/login.ts +++ b/webclient/src/websocket/commands/session/login.ts @@ -1,13 +1,11 @@ -import { StatusEnum, WebSocketConnectOptions } from 'types'; +import { App, Enriched, Data } from '@app/types'; import { create } from '@bufbuild/protobuf'; import type { MessageInitShape } from '@bufbuild/protobuf'; import { CLIENT_CONFIG } from '../../config'; import webClient from '../../WebClient'; -import { Command_Login_ext, Command_LoginSchema } from 'generated/proto/session_commands_pb'; + import { hashPassword } from '../../utils'; import { SessionPersistence } from '../../persistence'; -import { Response_Login_ext } from 'generated/proto/response_login_pb'; -import { Response_ResponseCode } from 'generated/proto/response_pb'; import { disconnect, @@ -16,10 +14,10 @@ import { updateStatus, } from './'; -export function login(options: WebSocketConnectOptions, password?: string, passwordSalt?: string): void { +export function login(options: Omit, password?: string, passwordSalt?: string): void { const { userName, hashedPassword } = options; - const loginConfig: MessageInitShape = { + const loginConfig: MessageInitShape = { ...CLIENT_CONFIG, clientid: 'webatrice', userName, @@ -29,50 +27,52 @@ export function login(options: WebSocketConnectOptions, password?: string, passw }; const onLoginError = (message: string, extra?: () => void) => { - updateStatus(StatusEnum.DISCONNECTED, message); + updateStatus(App.StatusEnum.DISCONNECTED, message); extra?.(); SessionPersistence.loginFailed(); disconnect(); }; - webClient.protobuf.sendSessionCommand(Command_Login_ext, create(Command_LoginSchema, loginConfig), { - responseExt: Response_Login_ext, + webClient.protobuf.sendSessionCommand(Data.Command_Login_ext, create(Data.Command_LoginSchema, loginConfig), { + responseExt: Data.Response_Login_ext, onSuccess: (resp) => { const { buddyList, ignoreList, userInfo } = resp; SessionPersistence.updateBuddyList(buddyList); SessionPersistence.updateIgnoreList(ignoreList); SessionPersistence.updateUser(userInfo); - const { password: _password, ...safeConfig } = loginConfig; - SessionPersistence.loginSuccessful(safeConfig); + SessionPersistence.loginSuccessful({ hashedPassword: loginConfig.hashedPassword }); listUsers(); listRooms(); - updateStatus(StatusEnum.LOGGED_IN, 'Logged in.'); + updateStatus(App.StatusEnum.LOGGED_IN, 'Logged in.'); }, onResponseCode: { - [Response_ResponseCode.RespClientUpdateRequired]: () => + [Data.Response_ResponseCode.RespClientUpdateRequired]: () => onLoginError('Login failed: missing features'), - [Response_ResponseCode.RespWrongPassword]: () => + [Data.Response_ResponseCode.RespWrongPassword]: () => onLoginError('Login failed: incorrect username or password'), - [Response_ResponseCode.RespUsernameInvalid]: () => + [Data.Response_ResponseCode.RespUsernameInvalid]: () => onLoginError('Login failed: incorrect username or password'), - [Response_ResponseCode.RespWouldOverwriteOldSession]: () => + [Data.Response_ResponseCode.RespWouldOverwriteOldSession]: () => onLoginError('Login failed: duplicated user session'), - [Response_ResponseCode.RespUserIsBanned]: () => + [Data.Response_ResponseCode.RespUserIsBanned]: () => onLoginError('Login failed: banned user'), - [Response_ResponseCode.RespRegistrationRequired]: () => + [Data.Response_ResponseCode.RespRegistrationRequired]: () => onLoginError('Login failed: registration required'), - [Response_ResponseCode.RespClientIdRequired]: () => + [Data.Response_ResponseCode.RespClientIdRequired]: () => onLoginError('Login failed: missing client ID'), - [Response_ResponseCode.RespContextError]: () => + [Data.Response_ResponseCode.RespContextError]: () => onLoginError('Login failed: server error'), - [Response_ResponseCode.RespAccountNotActivated]: () => + [Data.Response_ResponseCode.RespAccountNotActivated]: () => onLoginError('Login failed: account not activated', () => { - const { password: _p, newPassword: _np, ...safeOptions } = options; - SessionPersistence.accountAwaitingActivation(safeOptions); + SessionPersistence.accountAwaitingActivation({ + host: options.host, + port: options.port, + userName: options.userName, + }); } ), }, diff --git a/webclient/src/websocket/commands/session/message.ts b/webclient/src/websocket/commands/session/message.ts index b310010be..4c0aba16b 100644 --- a/webclient/src/websocket/commands/session/message.ts +++ b/webclient/src/websocket/commands/session/message.ts @@ -1,7 +1,7 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_Message_ext, Command_MessageSchema } from 'generated/proto/session_commands_pb'; +import { Data } from '@app/types'; export function message(userName: string, message: string): void { - webClient.protobuf.sendSessionCommand(Command_Message_ext, create(Command_MessageSchema, { userName, message })); + webClient.protobuf.sendSessionCommand(Data.Command_Message_ext, create(Data.Command_MessageSchema, { userName, message })); } diff --git a/webclient/src/websocket/commands/session/ping.ts b/webclient/src/websocket/commands/session/ping.ts index bb015094a..aef81d2b1 100644 --- a/webclient/src/websocket/commands/session/ping.ts +++ b/webclient/src/websocket/commands/session/ping.ts @@ -1,9 +1,9 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_Ping_ext, Command_PingSchema } from 'generated/proto/session_commands_pb'; +import { Data } from '@app/types'; export function ping(pingReceived: () => void): void { - webClient.protobuf.sendSessionCommand(Command_Ping_ext, create(Command_PingSchema), { + webClient.protobuf.sendSessionCommand(Data.Command_Ping_ext, create(Data.Command_PingSchema), { onResponse: () => pingReceived(), }); } diff --git a/webclient/src/websocket/commands/session/register.ts b/webclient/src/websocket/commands/session/register.ts index 8166f9f8e..fa5857fcb 100644 --- a/webclient/src/websocket/commands/session/register.ts +++ b/webclient/src/websocket/commands/session/register.ts @@ -1,22 +1,19 @@ -import { ServerRegisterParams } from 'store'; -import { StatusEnum, WebSocketConnectOptions } from 'types'; +import { App, Enriched, Data } from '@app/types'; import { create, getExtension } from '@bufbuild/protobuf'; import type { MessageInitShape } from '@bufbuild/protobuf'; import { CLIENT_CONFIG } from '../../config'; import webClient from '../../WebClient'; -import { Command_Register_ext, Command_RegisterSchema } from 'generated/proto/session_commands_pb'; + import { SessionPersistence } from '../../persistence'; import { hashPassword } from '../../utils'; -import { Response_ResponseCode } from 'generated/proto/response_pb'; -import { Response_Register_ext } from 'generated/proto/response_register_pb'; import { login, disconnect, updateStatus } from './'; -export function register(options: WebSocketConnectOptions, password?: string, passwordSalt?: string): void { - const { userName, email, country, realName } = options as ServerRegisterParams; +export function register(options: Omit, password?: string, passwordSalt?: string): void { + const { userName, email, country, realName } = options; - const params: MessageInitShape = { + const params: MessageInitShape = { ...CLIENT_CONFIG, userName, email, @@ -29,45 +26,53 @@ export function register(options: WebSocketConnectOptions, password?: string, pa const onRegistrationError = (action: () => void) => { action(); - updateStatus(StatusEnum.DISCONNECTED, 'Registration failed'); + updateStatus(App.StatusEnum.DISCONNECTED, 'Registration failed'); disconnect(); }; - webClient.protobuf.sendSessionCommand(Command_Register_ext, create(Command_RegisterSchema, params), { + webClient.protobuf.sendSessionCommand(Data.Command_Register_ext, create(Data.Command_RegisterSchema, params), { onResponseCode: { - [Response_ResponseCode.RespRegistrationAccepted]: () => { - login(options, password, passwordSalt); + [Data.Response_ResponseCode.RespRegistrationAccepted]: () => { + login({ + host: options.host, + port: options.port, + userName: options.userName, + reason: App.WebSocketConnectReason.LOGIN, + }, password, passwordSalt); SessionPersistence.registrationSuccess(); }, - [Response_ResponseCode.RespRegistrationAcceptedNeedsActivation]: () => { - updateStatus(StatusEnum.DISCONNECTED, 'Registration accepted, awaiting activation'); - const { password: _p, newPassword: _np, ...safeOptions } = options; - SessionPersistence.accountAwaitingActivation(safeOptions); + [Data.Response_ResponseCode.RespRegistrationAcceptedNeedsActivation]: () => { + updateStatus(App.StatusEnum.DISCONNECTED, 'Registration accepted, awaiting activation'); + SessionPersistence.accountAwaitingActivation({ + host: options.host, + port: options.port, + userName: options.userName, + }); disconnect(); }, - [Response_ResponseCode.RespUserAlreadyExists]: () => onRegistrationError( + [Data.Response_ResponseCode.RespUserAlreadyExists]: () => onRegistrationError( () => SessionPersistence.registrationUserNameError('Username is taken') ), - [Response_ResponseCode.RespUsernameInvalid]: () => onRegistrationError( + [Data.Response_ResponseCode.RespUsernameInvalid]: () => onRegistrationError( () => SessionPersistence.registrationUserNameError('Invalid username') ), - [Response_ResponseCode.RespPasswordTooShort]: () => onRegistrationError( + [Data.Response_ResponseCode.RespPasswordTooShort]: () => onRegistrationError( () => SessionPersistence.registrationPasswordError('Your password was too short') ), - [Response_ResponseCode.RespEmailRequiredToRegister]: () => onRegistrationError( + [Data.Response_ResponseCode.RespEmailRequiredToRegister]: () => onRegistrationError( () => SessionPersistence.registrationRequiresEmail() ), - [Response_ResponseCode.RespEmailBlackListed]: () => onRegistrationError( + [Data.Response_ResponseCode.RespEmailBlackListed]: () => onRegistrationError( () => SessionPersistence.registrationEmailError('This email provider has been blocked') ), - [Response_ResponseCode.RespTooManyRequests]: () => onRegistrationError( + [Data.Response_ResponseCode.RespTooManyRequests]: () => onRegistrationError( () => SessionPersistence.registrationEmailError('Max accounts reached for this email') ), - [Response_ResponseCode.RespRegistrationDisabled]: () => onRegistrationError( + [Data.Response_ResponseCode.RespRegistrationDisabled]: () => onRegistrationError( () => SessionPersistence.registrationFailed('Registration is currently disabled') ), - [Response_ResponseCode.RespUserIsBanned]: (raw) => { - const register = getExtension(raw, Response_Register_ext); + [Data.Response_ResponseCode.RespUserIsBanned]: (raw) => { + const register = getExtension(raw, Data.Response_Register_ext); onRegistrationError( () => SessionPersistence.registrationFailed(register.deniedReasonStr, Number(register.deniedEndTime)) ); diff --git a/webclient/src/websocket/commands/session/removeFromList.ts b/webclient/src/websocket/commands/session/removeFromList.ts index 033701815..d4ef36b01 100644 --- a/webclient/src/websocket/commands/session/removeFromList.ts +++ b/webclient/src/websocket/commands/session/removeFromList.ts @@ -1,7 +1,8 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_RemoveFromList_ext, Command_RemoveFromListSchema } from 'generated/proto/session_commands_pb'; + import { SessionPersistence } from '../../persistence'; +import { Data } from '@app/types'; export function removeFromBuddyList(userName: string): void { removeFromList('buddy', userName); @@ -12,7 +13,7 @@ export function removeFromIgnoreList(userName: string): void { } export function removeFromList(list: string, userName: string): void { - webClient.protobuf.sendSessionCommand(Command_RemoveFromList_ext, create(Command_RemoveFromListSchema, { list, userName }), { + webClient.protobuf.sendSessionCommand(Data.Command_RemoveFromList_ext, create(Data.Command_RemoveFromListSchema, { list, userName }), { onSuccess: () => { SessionPersistence.removeFromList(list, userName); }, diff --git a/webclient/src/websocket/commands/session/replayDeleteMatch.ts b/webclient/src/websocket/commands/session/replayDeleteMatch.ts index 09f99c84a..121890bc4 100644 --- a/webclient/src/websocket/commands/session/replayDeleteMatch.ts +++ b/webclient/src/websocket/commands/session/replayDeleteMatch.ts @@ -1,10 +1,11 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_ReplayDeleteMatchSchema, Command_ReplayDeleteMatch_ext } from 'generated/proto/command_replay_delete_match_pb'; + import { SessionPersistence } from '../../persistence'; +import { Data } from '@app/types'; export function replayDeleteMatch(gameId: number): void { - webClient.protobuf.sendSessionCommand(Command_ReplayDeleteMatch_ext, create(Command_ReplayDeleteMatchSchema, { gameId }), { + webClient.protobuf.sendSessionCommand(Data.Command_ReplayDeleteMatch_ext, create(Data.Command_ReplayDeleteMatchSchema, { gameId }), { onSuccess: () => { SessionPersistence.replayDeleteMatch(gameId); }, diff --git a/webclient/src/websocket/commands/session/replayGetCode.ts b/webclient/src/websocket/commands/session/replayGetCode.ts index 8c5b1121c..8e7a9b9b0 100644 --- a/webclient/src/websocket/commands/session/replayGetCode.ts +++ b/webclient/src/websocket/commands/session/replayGetCode.ts @@ -1,11 +1,10 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_ReplayGetCodeSchema, Command_ReplayGetCode_ext } from 'generated/proto/command_replay_get_code_pb'; -import { Response_ReplayGetCode_ext } from 'generated/proto/response_replay_get_code_pb'; +import { Data } from '@app/types'; export function replayGetCode(gameId: number, onCodeReceived: (code: string) => void): void { - webClient.protobuf.sendSessionCommand(Command_ReplayGetCode_ext, create(Command_ReplayGetCodeSchema, { gameId }), { - responseExt: Response_ReplayGetCode_ext, + webClient.protobuf.sendSessionCommand(Data.Command_ReplayGetCode_ext, create(Data.Command_ReplayGetCodeSchema, { gameId }), { + responseExt: Data.Response_ReplayGetCode_ext, onSuccess: (response) => { onCodeReceived(response.replayCode); }, diff --git a/webclient/src/websocket/commands/session/replayList.ts b/webclient/src/websocket/commands/session/replayList.ts index 7fd45a3b2..c5752267c 100644 --- a/webclient/src/websocket/commands/session/replayList.ts +++ b/webclient/src/websocket/commands/session/replayList.ts @@ -1,12 +1,12 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_ReplayListSchema, Command_ReplayList_ext } from 'generated/proto/command_replay_list_pb'; + import { SessionPersistence } from '../../persistence'; -import { Response_ReplayList_ext } from 'generated/proto/response_replay_list_pb'; +import { Data } from '@app/types'; export function replayList(): void { - webClient.protobuf.sendSessionCommand(Command_ReplayList_ext, create(Command_ReplayListSchema), { - responseExt: Response_ReplayList_ext, + webClient.protobuf.sendSessionCommand(Data.Command_ReplayList_ext, create(Data.Command_ReplayListSchema), { + responseExt: Data.Response_ReplayList_ext, onSuccess: (response) => { SessionPersistence.replayList(response.matchList); }, diff --git a/webclient/src/websocket/commands/session/replayModifyMatch.ts b/webclient/src/websocket/commands/session/replayModifyMatch.ts index b104a34ec..a5fd84fe8 100644 --- a/webclient/src/websocket/commands/session/replayModifyMatch.ts +++ b/webclient/src/websocket/commands/session/replayModifyMatch.ts @@ -1,12 +1,17 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_ReplayModifyMatchSchema, Command_ReplayModifyMatch_ext } from 'generated/proto/command_replay_modify_match_pb'; + import { SessionPersistence } from '../../persistence'; +import { Data } from '@app/types'; export function replayModifyMatch(gameId: number, doNotHide: boolean): void { - webClient.protobuf.sendSessionCommand(Command_ReplayModifyMatch_ext, create(Command_ReplayModifyMatchSchema, { gameId, doNotHide }), { - onSuccess: () => { - SessionPersistence.replayModifyMatch(gameId, doNotHide); - }, - }); + webClient.protobuf.sendSessionCommand( + Data.Command_ReplayModifyMatch_ext, + create(Data.Command_ReplayModifyMatchSchema, { gameId, doNotHide }), + { + onSuccess: () => { + SessionPersistence.replayModifyMatch(gameId, doNotHide); + }, + } + ); } diff --git a/webclient/src/websocket/commands/session/replaySubmitCode.ts b/webclient/src/websocket/commands/session/replaySubmitCode.ts index 0d55a79db..351439fb2 100644 --- a/webclient/src/websocket/commands/session/replaySubmitCode.ts +++ b/webclient/src/websocket/commands/session/replaySubmitCode.ts @@ -1,13 +1,13 @@ import { create } from '@bufbuild/protobuf'; import webClient from '../../WebClient'; -import { Command_ReplaySubmitCodeSchema, Command_ReplaySubmitCode_ext } from 'generated/proto/command_replay_submit_code_pb'; +import { Data } from '@app/types'; export function replaySubmitCode( replayCode: string, onSuccess?: () => void, onError?: (responseCode: number) => void, ): void { - webClient.protobuf.sendSessionCommand(Command_ReplaySubmitCode_ext, create(Command_ReplaySubmitCodeSchema, { replayCode }), { + webClient.protobuf.sendSessionCommand(Data.Command_ReplaySubmitCode_ext, create(Data.Command_ReplaySubmitCodeSchema, { replayCode }), { onSuccess, onError, }); diff --git a/webclient/src/websocket/commands/session/requestPasswordSalt.ts b/webclient/src/websocket/commands/session/requestPasswordSalt.ts index 0e9d40e87..14f8223d4 100644 --- a/webclient/src/websocket/commands/session/requestPasswordSalt.ts +++ b/webclient/src/websocket/commands/session/requestPasswordSalt.ts @@ -1,15 +1,10 @@ -import { RequestPasswordSaltParams } from 'store'; -import { StatusEnum, WebSocketConnectOptions, WebSocketConnectReason } from 'types'; +import { App, Enriched, Data } from '@app/types'; import { create } from '@bufbuild/protobuf'; import { CLIENT_CONFIG } from '../../config'; import webClient from '../../WebClient'; -import { - Command_RequestPasswordSalt_ext, Command_RequestPasswordSaltSchema, -} from 'generated/proto/session_commands_pb'; + import { SessionPersistence } from '../../persistence'; -import { Response_PasswordSalt_ext } from 'generated/proto/response_password_salt_pb'; -import { Response_ResponseCode } from 'generated/proto/response_pb'; import { activate, @@ -19,15 +14,20 @@ import { updateStatus } from './'; -export function requestPasswordSalt(options: WebSocketConnectOptions, password?: string, newPassword?: string): void { - const { userName } = options as RequestPasswordSaltParams; +type PasswordSaltOptions = + | Omit + | Omit + | Omit; + +export function requestPasswordSalt(options: PasswordSaltOptions, password?: string, newPassword?: string): void { + const { userName } = options; const onFailure = () => { switch (options.reason) { - case WebSocketConnectReason.ACTIVATE_ACCOUNT: + case App.WebSocketConnectReason.ACTIVATE_ACCOUNT: SessionPersistence.accountActivationFailed(); break; - case WebSocketConnectReason.PASSWORD_RESET: + case App.WebSocketConnectReason.PASSWORD_RESET: SessionPersistence.resetPasswordFailed(); break; default: @@ -36,19 +36,19 @@ export function requestPasswordSalt(options: WebSocketConnectOptions, password?: disconnect(); }; - webClient.protobuf.sendSessionCommand(Command_RequestPasswordSalt_ext, create(Command_RequestPasswordSaltSchema, { + webClient.protobuf.sendSessionCommand(Data.Command_RequestPasswordSalt_ext, create(Data.Command_RequestPasswordSaltSchema, { ...CLIENT_CONFIG, userName, }), { - responseExt: Response_PasswordSalt_ext, + responseExt: Data.Response_PasswordSalt_ext, onSuccess: (resp) => { const passwordSalt = resp?.passwordSalt; switch (options.reason) { - case WebSocketConnectReason.ACTIVATE_ACCOUNT: + case App.WebSocketConnectReason.ACTIVATE_ACCOUNT: activate(options, password, passwordSalt); break; - case WebSocketConnectReason.PASSWORD_RESET: + case App.WebSocketConnectReason.PASSWORD_RESET: forgotPasswordReset(options, newPassword, passwordSalt); break; default: @@ -56,13 +56,13 @@ export function requestPasswordSalt(options: WebSocketConnectOptions, password?: } }, onResponseCode: { - [Response_ResponseCode.RespRegistrationRequired]: () => { - updateStatus(StatusEnum.DISCONNECTED, 'Login failed: registration required'); + [Data.Response_ResponseCode.RespRegistrationRequired]: () => { + updateStatus(App.StatusEnum.DISCONNECTED, 'Login failed: registration required'); onFailure(); }, }, onError: () => { - updateStatus(StatusEnum.DISCONNECTED, 'Login failed: Unknown Reason'); + updateStatus(App.StatusEnum.DISCONNECTED, 'Login failed: Unknown Reason'); onFailure(); }, }); diff --git a/webclient/src/websocket/commands/session/sessionCommands-complex.spec.ts b/webclient/src/websocket/commands/session/sessionCommands-complex.spec.ts index f82dc2b1d..59bd3feb1 100644 --- a/webclient/src/websocket/commands/session/sessionCommands-complex.spec.ts +++ b/webclient/src/websocket/commands/session/sessionCommands-complex.spec.ts @@ -30,24 +30,11 @@ import { makeCallbackHelpers } from '../../__mocks__/callbackHelpers'; import { SessionPersistence } from '../../persistence'; import webClient from '../../WebClient'; import * as SessionIndexMocks from './'; -import { StatusEnum, WebSocketConnectReason } from 'types'; +import { App, Enriched, Data } from '@app/types'; import { hashPassword, generateSalt, passwordSaltSupported } from '../../utils'; -import { Response_ResponseCode } from 'generated/proto/response_pb'; -import { - Command_Activate_ext, - Command_ForgotPasswordChallenge_ext, - Command_ForgotPasswordRequest_ext, - Command_ForgotPasswordReset_ext, - Command_Login_ext, - Command_Register_ext, - Command_RequestPasswordSalt_ext, -} from 'generated/proto/session_commands_pb'; -import { Response_ForgotPasswordRequest_ext } from 'generated/proto/response_forgotpasswordrequest_pb'; -import { Response_Login_ext } from 'generated/proto/response_login_pb'; -import { Response_PasswordSalt_ext } from 'generated/proto/response_password_salt_pb'; -import { Response_Register_ext, Response_RegisterSchema } from 'generated/proto/response_register_pb'; + import { create, setExtension } from '@bufbuild/protobuf'; -import { ResponseSchema } from 'generated/proto/response_pb'; + import { connect } from './connect'; import { updateStatus } from './updateStatus'; import { login } from './login'; @@ -63,8 +50,61 @@ const { invokeOnSuccess, invokeResponseCode, invokeOnError } = makeCallbackHelpe 2 ); +const baseTransport = { host: 'h', port: '1' }; +const makeLoginOpts = (overrides: Partial = {}): Enriched.LoginConnectOptions => ({ + ...baseTransport, + userName: 'alice', + reason: App.WebSocketConnectReason.LOGIN, + ...overrides, +}); +const makeRegisterOpts = ( + overrides: Partial = {} +): Enriched.RegisterConnectOptions => ({ + ...baseTransport, + userName: 'alice', + password: 'pw', + email: 'a@b.com', + country: 'US', + realName: 'Al', + reason: App.WebSocketConnectReason.REGISTER, + ...overrides, +}); +const makeActivateOpts = ( + overrides: Partial = {} +): Enriched.ActivateConnectOptions => ({ + ...baseTransport, + userName: 'alice', + token: 'tok', + reason: App.WebSocketConnectReason.ACTIVATE_ACCOUNT, + ...overrides, +}); +const makeForgotRequestOpts = (): Enriched.PasswordResetRequestConnectOptions => ({ + ...baseTransport, + userName: 'alice', + reason: App.WebSocketConnectReason.PASSWORD_RESET_REQUEST, +}); +const makeForgotChallengeOpts = (): Enriched.PasswordResetChallengeConnectOptions => ({ + ...baseTransport, + userName: 'alice', + email: 'a@b.com', + reason: App.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE, +}); +const makeForgotResetOpts = (): Enriched.PasswordResetConnectOptions => ({ + ...baseTransport, + userName: 'alice', + token: 'tok', + newPassword: 'newpw', + reason: App.WebSocketConnectReason.PASSWORD_RESET, +}); +const makeSaltOpts = ( + reason: App.WebSocketConnectReason, + extras: Record = {} +) => ({ ...baseTransport, userName: 'alice', reason, ...extras } as + | Enriched.LoginConnectOptions + | Enriched.ActivateConnectOptions + | Enriched.PasswordResetConnectOptions); + beforeEach(() => { - vi.clearAllMocks(); (hashPassword as Mock).mockReturnValue('hashed_pw'); (generateSalt as Mock).mockReturnValue('randSalt'); (passwordSaltSupported as Mock).mockReturnValue(0); @@ -76,45 +116,46 @@ beforeEach(() => { describe('connect', () => { it('calls updateStatus CONNECTING for LOGIN reason', () => { - connect({ host: 'h', port: 1 } as any, WebSocketConnectReason.LOGIN); - expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTING, 'Connecting...'); + connect({ host: 'h', port: '1', userName: 'u', reason: App.WebSocketConnectReason.LOGIN }); + expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(App.StatusEnum.CONNECTING, 'Connecting...'); expect(webClient.connect).toHaveBeenCalled(); }); it('calls updateStatus CONNECTING for REGISTER reason', () => { - connect({ host: 'h', port: 1 } as any, WebSocketConnectReason.REGISTER); - expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTING, 'Connecting...'); + connect(makeRegisterOpts({ userName: 'u', realName: 'U' })); + expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(App.StatusEnum.CONNECTING, 'Connecting...'); }); it('calls updateStatus CONNECTING for ACTIVATE_ACCOUNT reason', () => { - connect({ host: 'h', port: 1 } as any, WebSocketConnectReason.ACTIVATE_ACCOUNT); - expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTING, 'Connecting...'); + connect({ host: 'h', port: '1', userName: 'u', token: 'tok', reason: App.WebSocketConnectReason.ACTIVATE_ACCOUNT }); + expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(App.StatusEnum.CONNECTING, 'Connecting...'); }); it('calls updateStatus CONNECTING for PASSWORD_RESET_REQUEST reason', () => { - connect({ host: 'h', port: 1 } as any, WebSocketConnectReason.PASSWORD_RESET_REQUEST); - expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTING, 'Connecting...'); + connect({ host: 'h', port: '1', userName: 'u', reason: App.WebSocketConnectReason.PASSWORD_RESET_REQUEST }); + expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(App.StatusEnum.CONNECTING, 'Connecting...'); }); it('calls updateStatus CONNECTING for PASSWORD_RESET_CHALLENGE reason', () => { - connect({ host: 'h', port: 1 } as any, WebSocketConnectReason.PASSWORD_RESET_CHALLENGE); - expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTING, 'Connecting...'); + connect({ host: 'h', port: '1', userName: 'u', email: 'a@b.com', reason: App.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE }); + expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(App.StatusEnum.CONNECTING, 'Connecting...'); }); it('calls updateStatus CONNECTING for PASSWORD_RESET reason', () => { - connect({ host: 'h', port: 1 } as any, WebSocketConnectReason.PASSWORD_RESET); - expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTING, 'Connecting...'); + connect({ host: 'h', port: '1', userName: 'u', token: 'tok', newPassword: 'newpw', reason: App.WebSocketConnectReason.PASSWORD_RESET }); + expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(App.StatusEnum.CONNECTING, 'Connecting...'); }); it('calls testConnect for TEST_CONNECTION reason', () => { - connect({ host: 'h', port: 1 } as any, WebSocketConnectReason.TEST_CONNECTION); + connect({ host: 'h', port: '1', reason: App.WebSocketConnectReason.TEST_CONNECTION }); expect(webClient.testConnect).toHaveBeenCalled(); expect(webClient.connect).not.toHaveBeenCalled(); }); it('calls updateStatus DISCONNECTED for unknown reason', () => { - connect({ host: 'h', port: 1 } as any, 999 as WebSocketConnectReason); - expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, expect.stringContaining('Unknown')); + const bogus = { host: 'h', port: '1', reason: 999 as App.WebSocketConnectReason }; + connect(bogus as Enriched.WebSocketConnectOptions); + expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(App.StatusEnum.DISCONNECTED, expect.stringContaining('Unknown')); }); }); @@ -124,9 +165,9 @@ describe('connect', () => { describe('updateStatus', () => { it('calls SessionPersistence.updateStatus and webClient.updateStatus', () => { - updateStatus(StatusEnum.CONNECTED, 'OK'); - expect(SessionPersistence.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTED, 'OK'); - expect(webClient.updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTED); + updateStatus(App.StatusEnum.CONNECTED, 'OK'); + expect(SessionPersistence.updateStatus).toHaveBeenCalledWith(App.StatusEnum.CONNECTED, 'OK'); + expect(webClient.updateStatus).toHaveBeenCalledWith(App.StatusEnum.CONNECTED); }); }); @@ -136,34 +177,34 @@ describe('updateStatus', () => { describe('login', () => { it('sends Command_Login with plain password when no salt', () => { - login({ userName: 'alice' } as any, 'pw'); + login(makeLoginOpts(), 'pw'); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Login_ext, + Data.Command_Login_ext, expect.objectContaining({ password: 'pw' }), - expect.objectContaining({ responseExt: Response_Login_ext }) + expect.objectContaining({ responseExt: Data.Response_Login_ext }) ); }); it('sends Command_Login with hashedPassword when salt is given', () => { - login({ userName: 'alice' } as any, 'pw', 'salt'); + login(makeLoginOpts(), 'pw', 'salt'); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Login_ext, + Data.Command_Login_ext, expect.objectContaining({ hashedPassword: 'hashed_pw' }), - expect.objectContaining({ responseExt: Response_Login_ext }) + expect.objectContaining({ responseExt: Data.Response_Login_ext }) ); }); it('uses options.hashedPassword if provided', () => { - login({ userName: 'alice', hashedPassword: 'pre_hashed' } as any, 'pw', 'salt'); + login(makeLoginOpts({ hashedPassword: 'pre_hashed' }), 'pw', 'salt'); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Login_ext, + Data.Command_Login_ext, expect.objectContaining({ hashedPassword: 'pre_hashed' }), - expect.objectContaining({ responseExt: Response_Login_ext }) + expect.objectContaining({ responseExt: Data.Response_Login_ext }) ); }); it('onSuccess dispatches buddy/ignore/user and calls listUsers/listRooms', () => { - login({ userName: 'alice' } as any, 'pw'); + login(makeLoginOpts(), 'pw'); const loginResp = { buddyList: [], ignoreList: [], userInfo: { name: 'alice' } }; invokeOnSuccess(loginResp, { responseCode: 0 }); expect(SessionPersistence.updateBuddyList).toHaveBeenCalledWith([]); @@ -172,11 +213,11 @@ describe('login', () => { expect(SessionPersistence.loginSuccessful).toHaveBeenCalled(); expect(SessionIndexMocks.listUsers).toHaveBeenCalled(); expect(SessionIndexMocks.listRooms).toHaveBeenCalled(); - expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.LOGGED_IN, 'Logged in.'); + expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(App.StatusEnum.LOGGED_IN, 'Logged in.'); }); it('onSuccess does NOT pass plaintext password to loginSuccessful', () => { - login({ userName: 'alice' } as any, 'secret'); + login(makeLoginOpts(), 'secret'); const loginResp = { buddyList: [], ignoreList: [], userInfo: { name: 'alice' } }; invokeOnSuccess(loginResp, { responseCode: 0 }); const calledWith = (SessionPersistence.loginSuccessful as Mock).mock.calls[0][0]; @@ -184,7 +225,7 @@ describe('login', () => { }); it('onSuccess passes hashedPassword to loginSuccessful when salt is used', () => { - login({ userName: 'alice' } as any, 'pw', 'salt'); + login({ host: 'h', port: '1', userName: 'alice', reason: App.WebSocketConnectReason.LOGIN }, 'pw', 'salt'); const loginResp = { buddyList: [], ignoreList: [], userInfo: { name: 'alice' } }; invokeOnSuccess(loginResp, { responseCode: 0 }); const calledWith = (SessionPersistence.loginSuccessful as Mock).mock.calls[0][0]; @@ -192,57 +233,57 @@ describe('login', () => { }); it('onResponseCode RespClientUpdateRequired calls onLoginError', () => { - login({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(Response_ResponseCode.RespClientUpdateRequired); + login(makeLoginOpts(), 'pw'); + invokeResponseCode(Data.Response_ResponseCode.RespClientUpdateRequired); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); }); it('onResponseCode RespWrongPassword', () => { - login({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(Response_ResponseCode.RespWrongPassword); + login(makeLoginOpts(), 'pw'); + invokeResponseCode(Data.Response_ResponseCode.RespWrongPassword); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespUsernameInvalid', () => { - login({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(Response_ResponseCode.RespUsernameInvalid); + login(makeLoginOpts(), 'pw'); + invokeResponseCode(Data.Response_ResponseCode.RespUsernameInvalid); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespWouldOverwriteOldSession', () => { - login({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(Response_ResponseCode.RespWouldOverwriteOldSession); + login(makeLoginOpts(), 'pw'); + invokeResponseCode(Data.Response_ResponseCode.RespWouldOverwriteOldSession); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespUserIsBanned', () => { - login({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(Response_ResponseCode.RespUserIsBanned); + login(makeLoginOpts(), 'pw'); + invokeResponseCode(Data.Response_ResponseCode.RespUserIsBanned); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespRegistrationRequired', () => { - login({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(Response_ResponseCode.RespRegistrationRequired); + login(makeLoginOpts(), 'pw'); + invokeResponseCode(Data.Response_ResponseCode.RespRegistrationRequired); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespClientIdRequired', () => { - login({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(Response_ResponseCode.RespClientIdRequired); + login(makeLoginOpts(), 'pw'); + invokeResponseCode(Data.Response_ResponseCode.RespClientIdRequired); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespContextError', () => { - login({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(Response_ResponseCode.RespContextError); + login(makeLoginOpts(), 'pw'); + invokeResponseCode(Data.Response_ResponseCode.RespContextError); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); it('onResponseCode RespAccountNotActivated calls accountAwaitingActivation without password in options', () => { - login({ userName: 'alice', password: 'leaked' } as any, 'pw'); - invokeResponseCode(Response_ResponseCode.RespAccountNotActivated); + login(makeLoginOpts({ password: 'leaked' }), 'pw'); + invokeResponseCode(Data.Response_ResponseCode.RespAccountNotActivated); expect(SessionPersistence.accountAwaitingActivation).toHaveBeenCalledWith( expect.not.objectContaining({ password: expect.anything() }) ); @@ -250,7 +291,7 @@ describe('login', () => { }); it('onError calls onLoginError with unknown error message', () => { - login({ userName: 'alice' } as any, 'pw'); + login(makeLoginOpts(), 'pw'); invokeOnError(999); expect(SessionPersistence.loginFailed).toHaveBeenCalled(); }); @@ -262,40 +303,40 @@ describe('login', () => { describe('register', () => { it('sends Command_Register with plain password when no salt', () => { - register({ userName: 'alice', email: 'a@b.com', country: 'US', realName: 'Al' } as any, 'pw'); + register(makeRegisterOpts(), 'pw'); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Register_ext, + Data.Command_Register_ext, expect.objectContaining({ password: 'pw' }), expect.any(Object) ); }); it('uses hashedPassword when salt is provided', () => { - register({ userName: 'alice' } as any, 'pw', 'salt'); + register(makeRegisterOpts(), 'pw', 'salt'); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Register_ext, + Data.Command_Register_ext, expect.objectContaining({ hashedPassword: 'hashed_pw' }), expect.any(Object) ); }); it('RespRegistrationAccepted calls login without salt and registrationSuccess', () => { - register({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(Response_ResponseCode.RespRegistrationAccepted); + register(makeRegisterOpts(), 'pw'); + invokeResponseCode(Data.Response_ResponseCode.RespRegistrationAccepted); expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'pw', undefined); expect(SessionPersistence.registrationSuccess).toHaveBeenCalled(); }); it('RespRegistrationAccepted forwards salt to login', () => { - register({ userName: 'alice' } as any, 'pw', 'mySalt'); - invokeResponseCode(Response_ResponseCode.RespRegistrationAccepted); + register(makeRegisterOpts(), 'pw', 'mySalt'); + invokeResponseCode(Data.Response_ResponseCode.RespRegistrationAccepted); expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'pw', 'mySalt'); expect(SessionPersistence.registrationSuccess).toHaveBeenCalled(); }); it('RespRegistrationAcceptedNeedsActivation calls accountAwaitingActivation without password in options', () => { - register({ userName: 'alice', password: 'leaked' } as any, 'pw'); - invokeResponseCode(Response_ResponseCode.RespRegistrationAcceptedNeedsActivation); + register(makeRegisterOpts({ password: 'leaked' }), 'pw'); + invokeResponseCode(Data.Response_ResponseCode.RespRegistrationAcceptedNeedsActivation); expect(SessionPersistence.accountAwaitingActivation).toHaveBeenCalledWith( expect.not.objectContaining({ password: expect.anything() }) ); @@ -303,57 +344,59 @@ describe('register', () => { }); it('RespUserAlreadyExists calls registrationUserNameError', () => { - register({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(Response_ResponseCode.RespUserAlreadyExists); + register(makeRegisterOpts(), 'pw'); + invokeResponseCode(Data.Response_ResponseCode.RespUserAlreadyExists); expect(SessionPersistence.registrationUserNameError).toHaveBeenCalled(); }); it('RespUsernameInvalid calls registrationUserNameError', () => { - register({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(Response_ResponseCode.RespUsernameInvalid); + register(makeRegisterOpts(), 'pw'); + invokeResponseCode(Data.Response_ResponseCode.RespUsernameInvalid); expect(SessionPersistence.registrationUserNameError).toHaveBeenCalled(); }); it('RespPasswordTooShort calls registrationPasswordError', () => { - register({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(Response_ResponseCode.RespPasswordTooShort); + register(makeRegisterOpts(), 'pw'); + invokeResponseCode(Data.Response_ResponseCode.RespPasswordTooShort); expect(SessionPersistence.registrationPasswordError).toHaveBeenCalled(); }); it('RespEmailRequiredToRegister calls registrationRequiresEmail', () => { - register({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(Response_ResponseCode.RespEmailRequiredToRegister); + register(makeRegisterOpts(), 'pw'); + invokeResponseCode(Data.Response_ResponseCode.RespEmailRequiredToRegister); expect(SessionPersistence.registrationRequiresEmail).toHaveBeenCalled(); }); it('RespEmailBlackListed calls registrationEmailError', () => { - register({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(Response_ResponseCode.RespEmailBlackListed); + register(makeRegisterOpts(), 'pw'); + invokeResponseCode(Data.Response_ResponseCode.RespEmailBlackListed); expect(SessionPersistence.registrationEmailError).toHaveBeenCalled(); }); it('RespTooManyRequests calls registrationEmailError', () => { - register({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(Response_ResponseCode.RespTooManyRequests); + register(makeRegisterOpts(), 'pw'); + invokeResponseCode(Data.Response_ResponseCode.RespTooManyRequests); expect(SessionPersistence.registrationEmailError).toHaveBeenCalled(); }); it('RespRegistrationDisabled calls registrationFailed', () => { - register({ userName: 'alice' } as any, 'pw'); - invokeResponseCode(Response_ResponseCode.RespRegistrationDisabled); + register(makeRegisterOpts(), 'pw'); + invokeResponseCode(Data.Response_ResponseCode.RespRegistrationDisabled); expect(SessionPersistence.registrationFailed).toHaveBeenCalled(); }); it('RespUserIsBanned calls registrationFailed with deniedReasonStr and deniedEndTime', () => { - register({ userName: 'alice' } as any, 'pw'); - const raw = create(ResponseSchema, { responseCode: Response_ResponseCode.RespUserIsBanned }); - setExtension(raw, Response_Register_ext, create(Response_RegisterSchema, { deniedReasonStr: 'bad user', deniedEndTime: 9999n })); - invokeResponseCode(Response_ResponseCode.RespUserIsBanned, raw); + register(makeRegisterOpts(), 'pw'); + const raw = create(Data.ResponseSchema, { responseCode: Data.Response_ResponseCode.RespUserIsBanned }); + setExtension(raw, Data.Response_Register_ext, create(Data.Response_RegisterSchema, { + deniedReasonStr: 'bad user', deniedEndTime: 9999n, + })); + invokeResponseCode(Data.Response_ResponseCode.RespUserIsBanned, raw); expect(SessionPersistence.registrationFailed).toHaveBeenCalledWith('bad user', 9999); }); it('onError calls registrationFailed', () => { - register({ userName: 'alice' } as any, 'pw'); + register(makeRegisterOpts(), 'pw'); invokeOnError(); expect(SessionPersistence.registrationFailed).toHaveBeenCalled(); }); @@ -365,28 +408,28 @@ describe('register', () => { describe('activate', () => { it('sends Command_Activate with userName and token, not password', () => { - activate({ userName: 'alice', token: 'tok' } as any, 'pw'); + activate(makeActivateOpts(), 'pw'); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Activate_ext, + Data.Command_Activate_ext, expect.objectContaining({ userName: 'alice', token: 'tok' }), expect.any(Object) ); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Activate_ext, + Data.Command_Activate_ext, expect.not.objectContaining({ password: expect.anything() }), expect.any(Object) ); }); it('RespActivationAccepted calls accountActivationSuccess and forwards password+salt to login', () => { - activate({ userName: 'alice', token: 'tok' } as any, 'pw', 'salt'); - invokeResponseCode(Response_ResponseCode.RespActivationAccepted); + activate(makeActivateOpts(), 'pw', 'salt'); + invokeResponseCode(Data.Response_ResponseCode.RespActivationAccepted); expect(SessionPersistence.accountActivationSuccess).toHaveBeenCalled(); expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'pw', 'salt'); }); it('onError calls accountActivationFailed and disconnect', () => { - activate({ userName: 'alice', token: 'tok' } as any); + activate(makeActivateOpts()); invokeOnError(); expect(SessionPersistence.accountActivationFailed).toHaveBeenCalled(); expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); @@ -399,21 +442,21 @@ describe('activate', () => { describe('forgotPasswordChallenge', () => { it('sends Command_ForgotPasswordChallenge', () => { - forgotPasswordChallenge({ userName: 'alice', email: 'a@b.com' } as any); + forgotPasswordChallenge(makeForgotChallengeOpts()); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ForgotPasswordChallenge_ext, expect.any(Object), expect.any(Object) + Data.Command_ForgotPasswordChallenge_ext, expect.any(Object), expect.any(Object) ); }); it('onSuccess calls resetPassword and disconnect', () => { - forgotPasswordChallenge({ userName: 'alice', email: 'a@b.com' } as any); + forgotPasswordChallenge(makeForgotChallengeOpts()); invokeOnSuccess(); expect(SessionPersistence.resetPassword).toHaveBeenCalled(); expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); }); it('onError calls resetPasswordFailed and disconnect', () => { - forgotPasswordChallenge({ userName: 'alice', email: 'a@b.com' } as any); + forgotPasswordChallenge(makeForgotChallengeOpts()); invokeOnError(); expect(SessionPersistence.resetPasswordFailed).toHaveBeenCalled(); expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); @@ -426,16 +469,16 @@ describe('forgotPasswordChallenge', () => { describe('forgotPasswordRequest', () => { it('sends Command_ForgotPasswordRequest', () => { - forgotPasswordRequest({ userName: 'alice' } as any); + forgotPasswordRequest(makeForgotRequestOpts()); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ForgotPasswordRequest_ext, + Data.Command_ForgotPasswordRequest_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_ForgotPasswordRequest_ext }) + expect.objectContaining({ responseExt: Data.Response_ForgotPasswordRequest_ext }) ); }); it('onSuccess with challengeEmail calls resetPasswordChallenge', () => { - forgotPasswordRequest({ userName: 'alice' } as any); + forgotPasswordRequest(makeForgotRequestOpts()); const resp = { challengeEmail: true }; invokeOnSuccess(resp, { responseCode: 0 }); expect(SessionPersistence.resetPasswordChallenge).toHaveBeenCalled(); @@ -443,7 +486,7 @@ describe('forgotPasswordRequest', () => { }); it('onSuccess without challengeEmail calls resetPassword', () => { - forgotPasswordRequest({ userName: 'alice' } as any); + forgotPasswordRequest(makeForgotRequestOpts()); const resp = { challengeEmail: false }; invokeOnSuccess(resp, { responseCode: 0 }); expect(SessionPersistence.resetPassword).toHaveBeenCalled(); @@ -451,7 +494,7 @@ describe('forgotPasswordRequest', () => { }); it('onError calls resetPasswordFailed and disconnect', () => { - forgotPasswordRequest({ userName: 'alice' } as any); + forgotPasswordRequest(makeForgotRequestOpts()); invokeOnError(); expect(SessionPersistence.resetPasswordFailed).toHaveBeenCalled(); expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); @@ -464,32 +507,32 @@ describe('forgotPasswordRequest', () => { describe('forgotPasswordReset', () => { it('sends Command_ForgotPasswordReset with plain newPassword when no salt', () => { - forgotPasswordReset({ userName: 'alice', token: 'tok' } as any, 'newpw'); + forgotPasswordReset(makeForgotResetOpts(), 'newpw'); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ForgotPasswordReset_ext, + Data.Command_ForgotPasswordReset_ext, expect.objectContaining({ newPassword: 'newpw' }), expect.any(Object) ); }); it('sends hashed new password when salt provided', () => { - forgotPasswordReset({ userName: 'alice', token: 'tok' } as any, 'newpw', 'salt'); + forgotPasswordReset(makeForgotResetOpts(), 'newpw', 'salt'); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ForgotPasswordReset_ext, + Data.Command_ForgotPasswordReset_ext, expect.objectContaining({ hashedNewPassword: 'hashed_pw' }), expect.any(Object) ); }); it('onSuccess calls resetPasswordSuccess and disconnect', () => { - forgotPasswordReset({ userName: 'alice', token: 'tok' } as any, 'newpw'); + forgotPasswordReset(makeForgotResetOpts(), 'newpw'); invokeOnSuccess(); expect(SessionPersistence.resetPasswordSuccess).toHaveBeenCalled(); expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); }); it('onError calls resetPasswordFailed and disconnect', () => { - forgotPasswordReset({ userName: 'alice', token: 'tok' } as any, 'newpw'); + forgotPasswordReset(makeForgotResetOpts(), 'newpw'); invokeOnError(); expect(SessionPersistence.resetPasswordFailed).toHaveBeenCalled(); expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); @@ -502,57 +545,65 @@ describe('forgotPasswordReset', () => { describe('requestPasswordSalt', () => { it('sends Command_RequestPasswordSalt', () => { - requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any, 'pw'); + requestPasswordSalt(makeSaltOpts(App.WebSocketConnectReason.LOGIN), 'pw'); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_RequestPasswordSalt_ext, + Data.Command_RequestPasswordSalt_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_PasswordSalt_ext }) + expect.objectContaining({ responseExt: Data.Response_PasswordSalt_ext }) ); }); it('onSuccess with LOGIN reason forwards password+salt to login', () => { - requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any, 'pw'); + requestPasswordSalt(makeSaltOpts(App.WebSocketConnectReason.LOGIN), 'pw'); const resp = { passwordSalt: 'salt123' }; invokeOnSuccess(resp, { responseCode: 0 }); expect(SessionIndexMocks.login).toHaveBeenCalledWith(expect.any(Object), 'pw', 'salt123'); }); it('onSuccess with ACTIVATE_ACCOUNT reason forwards password+salt to activate', () => { - requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.ACTIVATE_ACCOUNT } as any, 'pw'); + requestPasswordSalt(makeSaltOpts(App.WebSocketConnectReason.ACTIVATE_ACCOUNT, { token: 'tok' }), 'pw'); const resp = { passwordSalt: 'salt123' }; invokeOnSuccess(resp, { responseCode: 0 }); expect(SessionIndexMocks.activate).toHaveBeenCalledWith(expect.any(Object), 'pw', 'salt123'); }); it('onSuccess with PASSWORD_RESET reason forwards newPassword+salt to forgotPasswordReset', () => { - requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.PASSWORD_RESET } as any, undefined, 'newpw'); + requestPasswordSalt( + makeSaltOpts(App.WebSocketConnectReason.PASSWORD_RESET, { token: 'tok', newPassword: 'newpw' }), + undefined, + 'newpw' + ); const resp = { passwordSalt: 'salt123' }; invokeOnSuccess(resp, { responseCode: 0 }); expect(SessionIndexMocks.forgotPasswordReset).toHaveBeenCalledWith(expect.any(Object), 'newpw', 'salt123'); }); it('onResponseCode RespRegistrationRequired calls updateStatus and disconnect', () => { - requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any, 'pw'); - invokeResponseCode(Response_ResponseCode.RespRegistrationRequired); - expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, expect.any(String)); + requestPasswordSalt(makeSaltOpts(App.WebSocketConnectReason.LOGIN), 'pw'); + invokeResponseCode(Data.Response_ResponseCode.RespRegistrationRequired); + expect(SessionIndexMocks.updateStatus).toHaveBeenCalledWith(App.StatusEnum.DISCONNECTED, expect.any(String)); expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); }); it('onResponseCode RespRegistrationRequired with ACTIVATE_ACCOUNT calls accountActivationFailed', () => { - requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.ACTIVATE_ACCOUNT } as any, 'pw'); - invokeResponseCode(Response_ResponseCode.RespRegistrationRequired); + requestPasswordSalt(makeSaltOpts(App.WebSocketConnectReason.ACTIVATE_ACCOUNT, { token: 'tok' }), 'pw'); + invokeResponseCode(Data.Response_ResponseCode.RespRegistrationRequired); expect(SessionPersistence.accountActivationFailed).toHaveBeenCalled(); }); it('onError calls updateStatus DISCONNECTED and disconnect', () => { - requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.LOGIN } as any, 'pw'); + requestPasswordSalt(makeSaltOpts(App.WebSocketConnectReason.LOGIN), 'pw'); invokeOnError(); expect(SessionIndexMocks.updateStatus).toHaveBeenCalled(); expect(SessionIndexMocks.disconnect).toHaveBeenCalled(); }); it('onError with PASSWORD_RESET reason calls resetPasswordFailed', () => { - requestPasswordSalt({ userName: 'alice', reason: WebSocketConnectReason.PASSWORD_RESET } as any, undefined, 'newpw'); + requestPasswordSalt( + makeSaltOpts(App.WebSocketConnectReason.PASSWORD_RESET, { token: 'tok', newPassword: 'newpw' }), + undefined, + 'newpw' + ); invokeOnError(); expect(SessionPersistence.resetPasswordFailed).toHaveBeenCalled(); }); diff --git a/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts b/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts index b135e8d15..483dad436 100644 --- a/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts +++ b/webclient/src/websocket/commands/session/sessionCommands-simple.spec.ts @@ -22,7 +22,7 @@ vi.mock('../../utils', async () => { vi.mock('./', async () => { const actual = await vi.importActual('./'); const { makeSessionBarrelMock } = await import('../../__mocks__/sessionCommandMocks'); - return { ...(actual as any), ...makeSessionBarrelMock() }; + return { ...(actual as Record), ...makeSessionBarrelMock() }; }); import { Mock } from 'vitest'; @@ -31,38 +31,7 @@ import { SessionPersistence } from '../../persistence'; import { RoomPersistence } from '../../persistence'; import webClient from '../../WebClient'; import { hashPassword, generateSalt, passwordSaltSupported } from '../../utils'; -import { - Command_AccountEdit_ext, - Command_AccountImage_ext, - Command_AccountPassword_ext, - Command_AddToList_ext, - Command_GetGamesOfUser_ext, - Command_GetUserInfo_ext, - Command_JoinRoom_ext, - Command_ListRooms_ext, - Command_ListUsers_ext, - Command_Message_ext, - Command_Ping_ext, - Command_RemoveFromList_ext, -} from 'generated/proto/session_commands_pb'; -import { Command_DeckDel_ext } from 'generated/proto/command_deck_del_pb'; -import { Command_DeckDelDir_ext } from 'generated/proto/command_deck_del_dir_pb'; -import { Command_DeckList_ext } from 'generated/proto/command_deck_list_pb'; -import { Command_DeckNewDir_ext } from 'generated/proto/command_deck_new_dir_pb'; -import { Command_DeckUpload_ext } from 'generated/proto/command_deck_upload_pb'; -import { Command_ReplayDeleteMatch_ext } from 'generated/proto/command_replay_delete_match_pb'; -import { Command_ReplayGetCode_ext } from 'generated/proto/command_replay_get_code_pb'; -import { Command_ReplayList_ext } from 'generated/proto/command_replay_list_pb'; -import { Command_ReplayModifyMatch_ext } from 'generated/proto/command_replay_modify_match_pb'; -import { Command_ReplaySubmitCode_ext } from 'generated/proto/command_replay_submit_code_pb'; -import { Response_DeckList_ext } from 'generated/proto/response_deck_list_pb'; -import { Response_DeckUpload_ext } from 'generated/proto/response_deck_upload_pb'; -import { Response_GetGamesOfUser_ext } from 'generated/proto/response_get_games_of_user_pb'; -import { Response_GetUserInfo_ext } from 'generated/proto/response_get_user_info_pb'; -import { Response_JoinRoom_ext } from 'generated/proto/response_join_room_pb'; -import { Response_ListUsers_ext } from 'generated/proto/response_list_users_pb'; -import { Response_ReplayGetCode_ext } from 'generated/proto/response_replay_get_code_pb'; -import { Response_ReplayList_ext } from 'generated/proto/response_replay_list_pb'; + import { accountEdit } from './accountEdit'; import { accountImage } from './accountImage'; import { accountPassword } from './accountPassword'; @@ -86,6 +55,7 @@ import { addToList, addToBuddyList, addToIgnoreList } from './addToList'; import { removeFromList, removeFromBuddyList, removeFromIgnoreList } from './removeFromList'; import { replayGetCode } from './replayGetCode'; import { replaySubmitCode } from './replaySubmitCode'; +import { Data } from '@app/types'; const { invokeOnSuccess, invokeCallback } = makeCallbackHelpers( webClient.protobuf.sendSessionCommand as Mock, @@ -93,7 +63,6 @@ const { invokeOnSuccess, invokeCallback } = makeCallbackHelpers( ); beforeEach(() => { - vi.clearAllMocks(); (hashPassword as Mock).mockReturnValue('hashed_pw'); (generateSalt as Mock).mockReturnValue('randSalt'); (passwordSaltSupported as Mock).mockReturnValue(0); @@ -102,12 +71,10 @@ beforeEach(() => { // ---------------------------------------------------------------- describe('accountEdit', () => { - beforeEach(() => vi.clearAllMocks()); - it('sends Command_AccountEdit with correct params', () => { accountEdit('pw', 'Alice', 'a@b.com', 'US'); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_AccountEdit_ext, + Data.Command_AccountEdit_ext, expect.objectContaining({ passwordCheck: 'pw', realName: 'Alice', email: 'a@b.com', country: 'US' }), expect.any(Object) ); @@ -121,13 +88,11 @@ describe('accountEdit', () => { }); describe('accountImage', () => { - beforeEach(() => vi.clearAllMocks()); - it('sends Command_AccountImage', () => { const img = new Uint8Array([1, 2]); accountImage(img); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_AccountImage_ext, expect.objectContaining({ image: img }), expect.any(Object) + Data.Command_AccountImage_ext, expect.objectContaining({ image: img }), expect.any(Object) ); }); @@ -140,12 +105,10 @@ describe('accountImage', () => { }); describe('accountPassword', () => { - beforeEach(() => vi.clearAllMocks()); - it('sends Command_AccountPassword', () => { accountPassword('old', 'new', 'hashed'); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_AccountPassword_ext, + Data.Command_AccountPassword_ext, expect.objectContaining({ oldPassword: 'old', newPassword: 'new', hashedNewPassword: 'hashed' }), expect.any(Object) ); @@ -159,12 +122,10 @@ describe('accountPassword', () => { }); describe('deckDel', () => { - beforeEach(() => vi.clearAllMocks()); - it('sends Command_DeckDel', () => { deckDel(42); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_DeckDel_ext, + Data.Command_DeckDel_ext, expect.objectContaining({ deckId: 42 }), expect.any(Object) ); @@ -178,12 +139,10 @@ describe('deckDel', () => { }); describe('deckDelDir', () => { - beforeEach(() => vi.clearAllMocks()); - it('sends Command_DeckDelDir', () => { deckDelDir('/path'); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_DeckDelDir_ext, expect.objectContaining({ path: '/path' }), expect.any(Object) + Data.Command_DeckDelDir_ext, expect.objectContaining({ path: '/path' }), expect.any(Object) ); }); @@ -195,14 +154,12 @@ describe('deckDelDir', () => { }); describe('deckList', () => { - beforeEach(() => vi.clearAllMocks()); - it('sends Command_DeckList', () => { deckList(); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_DeckList_ext, + Data.Command_DeckList_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_DeckList_ext }) + expect.objectContaining({ responseExt: Data.Response_DeckList_ext }) ); }); @@ -215,12 +172,10 @@ describe('deckList', () => { }); describe('deckNewDir', () => { - beforeEach(() => vi.clearAllMocks()); - it('sends Command_DeckNewDir', () => { deckNewDir('/path', 'dir'); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_DeckNewDir_ext, expect.objectContaining({ path: '/path', dirName: 'dir' }), expect.any(Object) + Data.Command_DeckNewDir_ext, expect.objectContaining({ path: '/path', dirName: 'dir' }), expect.any(Object) ); }); @@ -232,14 +187,12 @@ describe('deckNewDir', () => { }); describe('deckUpload', () => { - beforeEach(() => vi.clearAllMocks()); - it('sends Command_DeckUpload', () => { deckUpload('/path', 1, 'content'); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_DeckUpload_ext, + Data.Command_DeckUpload_ext, expect.objectContaining({ path: '/path', deckId: 1, deckList: 'content' }), - expect.objectContaining({ responseExt: Response_DeckUpload_ext }) + expect.objectContaining({ responseExt: Data.Response_DeckUpload_ext }) ); }); @@ -252,8 +205,6 @@ describe('deckUpload', () => { }); describe('disconnect', () => { - beforeEach(() => vi.clearAllMocks()); - it('calls webClient.disconnect', () => { disconnect(); expect(webClient.disconnect).toHaveBeenCalled(); @@ -261,14 +212,12 @@ describe('disconnect', () => { }); describe('getGamesOfUser', () => { - beforeEach(() => vi.clearAllMocks()); - it('sends Command_GetGamesOfUser', () => { getGamesOfUser('alice'); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_GetGamesOfUser_ext, + Data.Command_GetGamesOfUser_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_GetGamesOfUser_ext }) + expect.objectContaining({ responseExt: Data.Response_GetGamesOfUser_ext }) ); }); @@ -281,14 +230,12 @@ describe('getGamesOfUser', () => { }); describe('getUserInfo', () => { - beforeEach(() => vi.clearAllMocks()); - it('sends Command_GetUserInfo', () => { getUserInfo('alice'); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_GetUserInfo_ext, + Data.Command_GetUserInfo_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_GetUserInfo_ext }) + expect.objectContaining({ responseExt: Data.Response_GetUserInfo_ext }) ); }); @@ -301,14 +248,12 @@ describe('getUserInfo', () => { }); describe('joinRoom', () => { - beforeEach(() => vi.clearAllMocks()); - it('sends Command_JoinRoom', () => { joinRoom(5); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_JoinRoom_ext, + Data.Command_JoinRoom_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_JoinRoom_ext }) + expect.objectContaining({ responseExt: Data.Response_JoinRoom_ext }) ); }); @@ -321,23 +266,19 @@ describe('joinRoom', () => { }); describe('listRooms (command)', () => { - beforeEach(() => vi.clearAllMocks()); - it('sends Command_ListRooms', () => { listRooms(); - expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith(Command_ListRooms_ext, expect.any(Object)); + expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith(Data.Command_ListRooms_ext, expect.any(Object)); }); }); describe('listUsers', () => { - beforeEach(() => vi.clearAllMocks()); - it('sends Command_ListUsers', () => { listUsers(); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ListUsers_ext, + Data.Command_ListUsers_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_ListUsers_ext }) + expect.objectContaining({ responseExt: Data.Response_ListUsers_ext }) ); }); @@ -350,24 +291,20 @@ describe('listUsers', () => { }); describe('message', () => { - beforeEach(() => vi.clearAllMocks()); - it('sends Command_Message', () => { message('bob', 'hi'); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_Message_ext, expect.objectContaining({ userName: 'bob', message: 'hi' }) + Data.Command_Message_ext, expect.objectContaining({ userName: 'bob', message: 'hi' }) ); }); }); describe('ping', () => { - beforeEach(() => vi.clearAllMocks()); - it('sends Command_Ping', () => { const pingReceived = vi.fn(); ping(pingReceived); - expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith(Command_Ping_ext, expect.any(Object), expect.any(Object)); + expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith(Data.Command_Ping_ext, expect.any(Object), expect.any(Object)); }); it('calls pingReceived via onResponse', () => { @@ -379,12 +316,10 @@ describe('ping', () => { }); describe('replayDeleteMatch', () => { - beforeEach(() => vi.clearAllMocks()); - it('sends Command_ReplayDeleteMatch', () => { replayDeleteMatch(7); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ReplayDeleteMatch_ext, + Data.Command_ReplayDeleteMatch_ext, expect.objectContaining({ gameId: 7 }), expect.any(Object) ); @@ -398,14 +333,12 @@ describe('replayDeleteMatch', () => { }); describe('replayList', () => { - beforeEach(() => vi.clearAllMocks()); - it('sends Command_ReplayList', () => { replayList(); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ReplayList_ext, + Data.Command_ReplayList_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_ReplayList_ext }) + expect.objectContaining({ responseExt: Data.Response_ReplayList_ext }) ); }); @@ -418,12 +351,10 @@ describe('replayList', () => { }); describe('replayModifyMatch', () => { - beforeEach(() => vi.clearAllMocks()); - it('sends Command_ReplayModifyMatch', () => { replayModifyMatch(7, true); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ReplayModifyMatch_ext, expect.objectContaining({ gameId: 7, doNotHide: true }), expect.any(Object) + Data.Command_ReplayModifyMatch_ext, expect.objectContaining({ gameId: 7, doNotHide: true }), expect.any(Object) ); }); @@ -435,12 +366,10 @@ describe('replayModifyMatch', () => { }); describe('addToList / addToBuddyList / addToIgnoreList', () => { - beforeEach(() => vi.clearAllMocks()); - it('addToBuddyList sends Command_AddToList with list=buddy', () => { addToBuddyList('alice'); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_AddToList_ext, + Data.Command_AddToList_ext, expect.objectContaining({ list: 'buddy' }), expect.any(Object) ); @@ -449,7 +378,7 @@ describe('addToList / addToBuddyList / addToIgnoreList', () => { it('addToIgnoreList sends Command_AddToList with list=ignore', () => { addToIgnoreList('bob'); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_AddToList_ext, + Data.Command_AddToList_ext, expect.objectContaining({ list: 'ignore' }), expect.any(Object) ); @@ -463,12 +392,10 @@ describe('addToList / addToBuddyList / addToIgnoreList', () => { }); describe('removeFromList / removeFromBuddyList / removeFromIgnoreList', () => { - beforeEach(() => vi.clearAllMocks()); - it('removeFromBuddyList sends Command_RemoveFromList with list=buddy', () => { removeFromBuddyList('alice'); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_RemoveFromList_ext, + Data.Command_RemoveFromList_ext, expect.objectContaining({ list: 'buddy' }), expect.any(Object) ); @@ -477,7 +404,7 @@ describe('removeFromList / removeFromBuddyList / removeFromIgnoreList', () => { it('removeFromIgnoreList sends Command_RemoveFromList with list=ignore', () => { removeFromIgnoreList('bob'); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_RemoveFromList_ext, + Data.Command_RemoveFromList_ext, expect.objectContaining({ list: 'ignore' }), expect.any(Object) ); @@ -491,14 +418,12 @@ describe('removeFromList / removeFromBuddyList / removeFromIgnoreList', () => { }); describe('replayGetCode', () => { - beforeEach(() => vi.clearAllMocks()); - it('sends Command_ReplayGetCode with gameId and responseExt', () => { replayGetCode(42, vi.fn()); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ReplayGetCode_ext, + Data.Command_ReplayGetCode_ext, expect.any(Object), - expect.objectContaining({ responseExt: Response_ReplayGetCode_ext }) + expect.objectContaining({ responseExt: Data.Response_ReplayGetCode_ext }) ); }); @@ -511,12 +436,10 @@ describe('replayGetCode', () => { }); describe('replaySubmitCode', () => { - beforeEach(() => vi.clearAllMocks()); - it('sends Command_ReplaySubmitCode with replayCode', () => { replaySubmitCode('42-abc123'); expect(webClient.protobuf.sendSessionCommand).toHaveBeenCalledWith( - Command_ReplaySubmitCode_ext, expect.objectContaining({ replayCode: '42-abc123' }), expect.any(Object) + Data.Command_ReplaySubmitCode_ext, expect.objectContaining({ replayCode: '42-abc123' }), expect.any(Object) ); }); diff --git a/webclient/src/websocket/commands/session/updateStatus.ts b/webclient/src/websocket/commands/session/updateStatus.ts index eddd774f8..504156f98 100644 --- a/webclient/src/websocket/commands/session/updateStatus.ts +++ b/webclient/src/websocket/commands/session/updateStatus.ts @@ -1,8 +1,8 @@ -import { StatusEnum } from 'types'; +import { App } from '@app/types'; import webClient from '../../WebClient'; import { SessionPersistence } from '../../persistence'; -export function updateStatus(status: StatusEnum, description: string): void { +export function updateStatus(status: App.StatusEnum, description: string): void { SessionPersistence.updateStatus(status, description); webClient.updateStatus(status); diff --git a/webclient/src/websocket/events/common/index.ts b/webclient/src/websocket/events/common/index.ts index c6832988d..cda3024f3 100644 --- a/webclient/src/websocket/events/common/index.ts +++ b/webclient/src/websocket/events/common/index.ts @@ -1,3 +1,3 @@ -import { SessionExtensionRegistry } from '../../services/protobuf-types'; +import type { SessionExtensionRegistry } from '../session'; export const CommonEvents: SessionExtensionRegistry = []; diff --git a/webclient/src/websocket/events/game/attachCard.ts b/webclient/src/websocket/events/game/attachCard.ts index 14c6e0354..389759f9f 100644 --- a/webclient/src/websocket/events/game/attachCard.ts +++ b/webclient/src/websocket/events/game/attachCard.ts @@ -1,7 +1,6 @@ -import type { Event_AttachCard } from 'generated/proto/event_attach_card_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function attachCard(data: Event_AttachCard, meta: GameEventMeta): void { +export function attachCard(data: Data.Event_AttachCard, meta: Enriched.GameEventMeta): void { GamePersistence.cardAttached(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/changeZoneProperties.ts b/webclient/src/websocket/events/game/changeZoneProperties.ts index 8454cb254..8bd066a8b 100644 --- a/webclient/src/websocket/events/game/changeZoneProperties.ts +++ b/webclient/src/websocket/events/game/changeZoneProperties.ts @@ -1,7 +1,6 @@ -import type { Event_ChangeZoneProperties } from 'generated/proto/event_change_zone_properties_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function changeZoneProperties(data: Event_ChangeZoneProperties, meta: GameEventMeta): void { +export function changeZoneProperties(data: Data.Event_ChangeZoneProperties, meta: Enriched.GameEventMeta): void { GamePersistence.zonePropertiesChanged(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/createArrow.ts b/webclient/src/websocket/events/game/createArrow.ts index 4685621d0..66933d636 100644 --- a/webclient/src/websocket/events/game/createArrow.ts +++ b/webclient/src/websocket/events/game/createArrow.ts @@ -1,7 +1,6 @@ -import type { Event_CreateArrow } from 'generated/proto/event_create_arrow_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function createArrow(data: Event_CreateArrow, meta: GameEventMeta): void { +export function createArrow(data: Data.Event_CreateArrow, meta: Enriched.GameEventMeta): void { GamePersistence.arrowCreated(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/createCounter.ts b/webclient/src/websocket/events/game/createCounter.ts index f7237ac06..500523179 100644 --- a/webclient/src/websocket/events/game/createCounter.ts +++ b/webclient/src/websocket/events/game/createCounter.ts @@ -1,7 +1,6 @@ -import type { Event_CreateCounter } from 'generated/proto/event_create_counter_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function createCounter(data: Event_CreateCounter, meta: GameEventMeta): void { +export function createCounter(data: Data.Event_CreateCounter, meta: Enriched.GameEventMeta): void { GamePersistence.counterCreated(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/createToken.ts b/webclient/src/websocket/events/game/createToken.ts index f4935661b..1dce664ff 100644 --- a/webclient/src/websocket/events/game/createToken.ts +++ b/webclient/src/websocket/events/game/createToken.ts @@ -1,7 +1,6 @@ -import type { Event_CreateToken } from 'generated/proto/event_create_token_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function createToken(data: Event_CreateToken, meta: GameEventMeta): void { +export function createToken(data: Data.Event_CreateToken, meta: Enriched.GameEventMeta): void { GamePersistence.tokenCreated(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/delCounter.ts b/webclient/src/websocket/events/game/delCounter.ts index 533425d03..f550f3bdd 100644 --- a/webclient/src/websocket/events/game/delCounter.ts +++ b/webclient/src/websocket/events/game/delCounter.ts @@ -1,7 +1,6 @@ -import type { Event_DelCounter } from 'generated/proto/event_del_counter_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function delCounter(data: Event_DelCounter, meta: GameEventMeta): void { +export function delCounter(data: Data.Event_DelCounter, meta: Enriched.GameEventMeta): void { GamePersistence.counterDeleted(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/deleteArrow.ts b/webclient/src/websocket/events/game/deleteArrow.ts index 5461fb2f1..a58ad59d3 100644 --- a/webclient/src/websocket/events/game/deleteArrow.ts +++ b/webclient/src/websocket/events/game/deleteArrow.ts @@ -1,7 +1,6 @@ -import type { Event_DeleteArrow } from 'generated/proto/event_delete_arrow_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function deleteArrow(data: Event_DeleteArrow, meta: GameEventMeta): void { +export function deleteArrow(data: Data.Event_DeleteArrow, meta: Enriched.GameEventMeta): void { GamePersistence.arrowDeleted(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/destroyCard.ts b/webclient/src/websocket/events/game/destroyCard.ts index 1c7c69de9..07bf5854a 100644 --- a/webclient/src/websocket/events/game/destroyCard.ts +++ b/webclient/src/websocket/events/game/destroyCard.ts @@ -1,7 +1,6 @@ -import type { Event_DestroyCard } from 'generated/proto/event_destroy_card_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function destroyCard(data: Event_DestroyCard, meta: GameEventMeta): void { +export function destroyCard(data: Data.Event_DestroyCard, meta: Enriched.GameEventMeta): void { GamePersistence.cardDestroyed(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/drawCards.ts b/webclient/src/websocket/events/game/drawCards.ts index e4c43f9f1..921716ba4 100644 --- a/webclient/src/websocket/events/game/drawCards.ts +++ b/webclient/src/websocket/events/game/drawCards.ts @@ -1,7 +1,6 @@ -import type { Event_DrawCards } from 'generated/proto/event_draw_cards_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function drawCards(data: Event_DrawCards, meta: GameEventMeta): void { +export function drawCards(data: Data.Event_DrawCards, meta: Enriched.GameEventMeta): void { GamePersistence.cardsDrawn(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/dumpZone.ts b/webclient/src/websocket/events/game/dumpZone.ts index 9ee513240..daf4a0fc8 100644 --- a/webclient/src/websocket/events/game/dumpZone.ts +++ b/webclient/src/websocket/events/game/dumpZone.ts @@ -1,7 +1,6 @@ -import type { Event_DumpZone } from 'generated/proto/event_dump_zone_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function dumpZone(data: Event_DumpZone, meta: GameEventMeta): void { +export function dumpZone(data: Data.Event_DumpZone, meta: Enriched.GameEventMeta): void { GamePersistence.zoneDumped(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/flipCard.ts b/webclient/src/websocket/events/game/flipCard.ts index 51ac479b3..cccf13906 100644 --- a/webclient/src/websocket/events/game/flipCard.ts +++ b/webclient/src/websocket/events/game/flipCard.ts @@ -1,7 +1,6 @@ -import type { Event_FlipCard } from 'generated/proto/event_flip_card_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function flipCard(data: Event_FlipCard, meta: GameEventMeta): void { +export function flipCard(data: Data.Event_FlipCard, meta: Enriched.GameEventMeta): void { GamePersistence.cardFlipped(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/gameClosed.ts b/webclient/src/websocket/events/game/gameClosed.ts index 9ebdaba5c..d604423df 100644 --- a/webclient/src/websocket/events/game/gameClosed.ts +++ b/webclient/src/websocket/events/game/gameClosed.ts @@ -1,6 +1,6 @@ -import { GameEventMeta } from 'types'; +import { Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function gameClosed(_data: {}, meta: GameEventMeta): void { +export function gameClosed(_data: {}, meta: Enriched.GameEventMeta): void { GamePersistence.gameClosed(meta.gameId); } diff --git a/webclient/src/websocket/events/game/gameEvents.spec.ts b/webclient/src/websocket/events/game/gameEvents.spec.ts index f1fd3571c..bfc94eeac 100644 --- a/webclient/src/websocket/events/game/gameEvents.spec.ts +++ b/webclient/src/websocket/events/game/gameEvents.spec.ts @@ -32,7 +32,10 @@ vi.mock('../../persistence', () => ({ }, })); +import { create } from '@bufbuild/protobuf'; +import { Data } from '@app/types'; import { GamePersistence } from '../../persistence'; + import { attachCard } from './attachCard'; import { changeZoneProperties } from './changeZoneProperties'; import { createArrow } from './createArrow'; @@ -63,14 +66,12 @@ import { setCardCounter } from './setCardCounter'; import { setCounter } from './setCounter'; import { shuffle } from './shuffle'; -beforeEach(() => vi.clearAllMocks()); - const meta = { gameId: 5, playerId: 2, context: null, secondsElapsed: 0, forcedByJudge: 0 }; describe('joinGame event', () => { it('delegates to GamePersistence.playerJoined with gameId from meta', () => { - const playerProperties = { playerId: 1 }; - const data = { playerProperties } as any; + const playerProperties = create(Data.ServerInfo_PlayerPropertiesSchema, { playerId: 1 }); + const data = { playerProperties }; joinGame(data, meta); expect(GamePersistence.playerJoined).toHaveBeenCalledWith(5, playerProperties); }); @@ -107,7 +108,7 @@ describe('kicked event', () => { describe('gameStateChanged event', () => { it('delegates to GamePersistence.gameStateChanged with gameId and full data', () => { - const data = { playerList: [] } as any; + const data = create(Data.Event_GameStateChangedSchema, { playerList: [] }); gameStateChanged(data, meta); expect(GamePersistence.gameStateChanged).toHaveBeenCalledWith(5, data); }); @@ -115,8 +116,8 @@ describe('gameStateChanged event', () => { describe('playerPropertiesChanged event', () => { it('delegates to GamePersistence.playerPropertiesChanged with gameId, playerId, properties', () => { - const playerProperties = { playerId: 2 } as any; - const data = { playerProperties } as any; + const playerProperties = create(Data.ServerInfo_PlayerPropertiesSchema, { playerId: 2 }); + const data = { playerProperties }; playerPropertiesChanged(data, meta); expect(GamePersistence.playerPropertiesChanged).toHaveBeenCalledWith(5, 2, playerProperties); }); @@ -124,7 +125,7 @@ describe('playerPropertiesChanged event', () => { describe('gameSay event', () => { it('delegates to GamePersistence.gameSay with gameId, playerId, message', () => { - const data = { message: 'gg' } as any; + const data = create(Data.Event_GameSaySchema, { message: 'gg' }); gameSay(data, meta); expect(GamePersistence.gameSay).toHaveBeenCalledWith(5, 2, 'gg'); }); @@ -132,7 +133,7 @@ describe('gameSay event', () => { describe('moveCard event', () => { it('delegates to GamePersistence.cardMoved with gameId, playerId and data', () => { - const data = { cardId: 3 } as any; + const data = create(Data.Event_MoveCardSchema, { cardId: 3 }); moveCard(data, meta); expect(GamePersistence.cardMoved).toHaveBeenCalledWith(5, 2, data); }); @@ -140,7 +141,7 @@ describe('moveCard event', () => { describe('flipCard event', () => { it('delegates to GamePersistence.cardFlipped with gameId, playerId and data', () => { - const data = { cardId: 3 } as any; + const data = create(Data.Event_FlipCardSchema, { cardId: 3 }); flipCard(data, meta); expect(GamePersistence.cardFlipped).toHaveBeenCalledWith(5, 2, data); }); @@ -148,7 +149,7 @@ describe('flipCard event', () => { describe('destroyCard event', () => { it('delegates to GamePersistence.cardDestroyed with gameId, playerId and data', () => { - const data = { cardId: 3 } as any; + const data = create(Data.Event_DestroyCardSchema, { cardId: 3 }); destroyCard(data, meta); expect(GamePersistence.cardDestroyed).toHaveBeenCalledWith(5, 2, data); }); @@ -156,7 +157,7 @@ describe('destroyCard event', () => { describe('attachCard event', () => { it('delegates to GamePersistence.cardAttached with gameId, playerId and data', () => { - const data = { cardId: 3 } as any; + const data = create(Data.Event_AttachCardSchema, { cardId: 3 }); attachCard(data, meta); expect(GamePersistence.cardAttached).toHaveBeenCalledWith(5, 2, data); }); @@ -164,7 +165,7 @@ describe('attachCard event', () => { describe('createToken event', () => { it('delegates to GamePersistence.tokenCreated with gameId, playerId and data', () => { - const data = { cardId: 3 } as any; + const data = create(Data.Event_CreateTokenSchema, { cardId: 3 }); createToken(data, meta); expect(GamePersistence.tokenCreated).toHaveBeenCalledWith(5, 2, data); }); @@ -172,7 +173,7 @@ describe('createToken event', () => { describe('setCardAttr event', () => { it('delegates to GamePersistence.cardAttrChanged with gameId, playerId and data', () => { - const data = { cardId: 3 } as any; + const data = create(Data.Event_SetCardAttrSchema, { cardId: 3 }); setCardAttr(data, meta); expect(GamePersistence.cardAttrChanged).toHaveBeenCalledWith(5, 2, data); }); @@ -180,7 +181,7 @@ describe('setCardAttr event', () => { describe('setCardCounter event', () => { it('delegates to GamePersistence.cardCounterChanged with gameId, playerId and data', () => { - const data = { cardId: 3 } as any; + const data = create(Data.Event_SetCardCounterSchema, { cardId: 3 }); setCardCounter(data, meta); expect(GamePersistence.cardCounterChanged).toHaveBeenCalledWith(5, 2, data); }); @@ -188,7 +189,7 @@ describe('setCardCounter event', () => { describe('createArrow event', () => { it('delegates to GamePersistence.arrowCreated with gameId, playerId and data', () => { - const data = { arrowInfo: {} } as any; + const data = create(Data.Event_CreateArrowSchema, {}); createArrow(data, meta); expect(GamePersistence.arrowCreated).toHaveBeenCalledWith(5, 2, data); }); @@ -196,7 +197,7 @@ describe('createArrow event', () => { describe('deleteArrow event', () => { it('delegates to GamePersistence.arrowDeleted with gameId, playerId and data', () => { - const data = { arrowId: 9 } as any; + const data = create(Data.Event_DeleteArrowSchema, { arrowId: 9 }); deleteArrow(data, meta); expect(GamePersistence.arrowDeleted).toHaveBeenCalledWith(5, 2, data); }); @@ -204,7 +205,7 @@ describe('deleteArrow event', () => { describe('createCounter event', () => { it('delegates to GamePersistence.counterCreated with gameId, playerId and data', () => { - const data = { counterInfo: {} } as any; + const data = create(Data.Event_CreateCounterSchema, {}); createCounter(data, meta); expect(GamePersistence.counterCreated).toHaveBeenCalledWith(5, 2, data); }); @@ -212,7 +213,7 @@ describe('createCounter event', () => { describe('setCounter event', () => { it('delegates to GamePersistence.counterSet with gameId, playerId and data', () => { - const data = { counterId: 1, value: 20 } as any; + const data = create(Data.Event_SetCounterSchema, { counterId: 1, value: 20 }); setCounter(data, meta); expect(GamePersistence.counterSet).toHaveBeenCalledWith(5, 2, data); }); @@ -220,7 +221,7 @@ describe('setCounter event', () => { describe('delCounter event', () => { it('delegates to GamePersistence.counterDeleted with gameId, playerId and data', () => { - const data = { counterId: 1 } as any; + const data = create(Data.Event_DelCounterSchema, { counterId: 1 }); delCounter(data, meta); expect(GamePersistence.counterDeleted).toHaveBeenCalledWith(5, 2, data); }); @@ -228,7 +229,7 @@ describe('delCounter event', () => { describe('drawCards event', () => { it('delegates to GamePersistence.cardsDrawn with gameId, playerId and data', () => { - const data = { number: 2, cards: [] } as any; + const data = create(Data.Event_DrawCardsSchema, { number: 2, cards: [] }); drawCards(data, meta); expect(GamePersistence.cardsDrawn).toHaveBeenCalledWith(5, 2, data); }); @@ -236,7 +237,7 @@ describe('drawCards event', () => { describe('revealCards event', () => { it('delegates to GamePersistence.cardsRevealed with gameId, playerId and data', () => { - const data = { zoneName: 'hand', cards: [] } as any; + const data = create(Data.Event_RevealCardsSchema, { zoneName: 'hand', cards: [] }); revealCards(data, meta); expect(GamePersistence.cardsRevealed).toHaveBeenCalledWith(5, 2, data); }); @@ -244,7 +245,7 @@ describe('revealCards event', () => { describe('shuffle event', () => { it('delegates to GamePersistence.zoneShuffled with gameId, playerId and data', () => { - const data = { zoneName: 'deck' } as any; + const data = create(Data.Event_ShuffleSchema, { zoneName: 'deck' }); shuffle(data, meta); expect(GamePersistence.zoneShuffled).toHaveBeenCalledWith(5, 2, data); }); @@ -252,7 +253,7 @@ describe('shuffle event', () => { describe('rollDie event', () => { it('delegates to GamePersistence.dieRolled with gameId, playerId and data', () => { - const data = { die: 6, result: 4 } as any; + const data = create(Data.Event_RollDieSchema, { die: 6, result: 4 }); rollDie(data, meta); expect(GamePersistence.dieRolled).toHaveBeenCalledWith(5, 2, data); }); @@ -260,7 +261,7 @@ describe('rollDie event', () => { describe('setActivePlayer event', () => { it('delegates to GamePersistence.activePlayerSet with gameId and activePlayerId', () => { - const data = { activePlayerId: 3 } as any; + const data = create(Data.Event_SetActivePlayerSchema, { activePlayerId: 3 }); setActivePlayer(data, meta); expect(GamePersistence.activePlayerSet).toHaveBeenCalledWith(5, 3); }); @@ -268,7 +269,7 @@ describe('setActivePlayer event', () => { describe('setActivePhase event', () => { it('delegates to GamePersistence.activePhaseSet with gameId and phase', () => { - const data = { phase: 4 } as any; + const data = create(Data.Event_SetActivePhaseSchema, { phase: 4 }); setActivePhase(data, meta); expect(GamePersistence.activePhaseSet).toHaveBeenCalledWith(5, 4); }); @@ -276,7 +277,7 @@ describe('setActivePhase event', () => { describe('reverseTurn event', () => { it('delegates to GamePersistence.turnReversed with gameId and reversed', () => { - const data = { reversed: true } as any; + const data = create(Data.Event_ReverseTurnSchema, { reversed: true }); reverseTurn(data, meta); expect(GamePersistence.turnReversed).toHaveBeenCalledWith(5, true); }); @@ -284,7 +285,7 @@ describe('reverseTurn event', () => { describe('dumpZone event', () => { it('delegates to GamePersistence.zoneDumped with gameId, playerId and data', () => { - const data = { zoneName: 'hand' } as any; + const data = create(Data.Event_DumpZoneSchema, { zoneName: 'hand' }); dumpZone(data, meta); expect(GamePersistence.zoneDumped).toHaveBeenCalledWith(5, 2, data); }); @@ -292,7 +293,7 @@ describe('dumpZone event', () => { describe('changeZoneProperties event', () => { it('delegates to GamePersistence.zonePropertiesChanged with gameId, playerId and data', () => { - const data = { zoneName: 'hand', alwaysRevealTopCard: true } as any; + const data = create(Data.Event_ChangeZonePropertiesSchema, { zoneName: 'hand', alwaysRevealTopCard: true }); changeZoneProperties(data, meta); expect(GamePersistence.zonePropertiesChanged).toHaveBeenCalledWith(5, 2, data); }); diff --git a/webclient/src/websocket/events/game/gameHostChanged.ts b/webclient/src/websocket/events/game/gameHostChanged.ts index 174b2fca1..212d182b0 100644 --- a/webclient/src/websocket/events/game/gameHostChanged.ts +++ b/webclient/src/websocket/events/game/gameHostChanged.ts @@ -1,10 +1,10 @@ -import { GameEventMeta } from 'types'; +import { Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; /** * Event_GameHostChanged carries no payload fields. * The new host is identified by GameEvent.player_id (meta.playerId). */ -export function gameHostChanged(_data: {}, meta: GameEventMeta): void { +export function gameHostChanged(_data: {}, meta: Enriched.GameEventMeta): void { GamePersistence.gameHostChanged(meta.gameId, meta.playerId); } diff --git a/webclient/src/websocket/events/game/gameSay.ts b/webclient/src/websocket/events/game/gameSay.ts index 66cfc06f9..3552f099f 100644 --- a/webclient/src/websocket/events/game/gameSay.ts +++ b/webclient/src/websocket/events/game/gameSay.ts @@ -1,7 +1,6 @@ -import type { Event_GameSay } from 'generated/proto/event_game_say_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function gameSay(data: Event_GameSay, meta: GameEventMeta): void { +export function gameSay(data: Data.Event_GameSay, meta: Enriched.GameEventMeta): void { GamePersistence.gameSay(meta.gameId, meta.playerId, data.message); } diff --git a/webclient/src/websocket/events/game/gameStateChanged.ts b/webclient/src/websocket/events/game/gameStateChanged.ts index 16f35b136..91342db72 100644 --- a/webclient/src/websocket/events/game/gameStateChanged.ts +++ b/webclient/src/websocket/events/game/gameStateChanged.ts @@ -1,7 +1,6 @@ -import type { Event_GameStateChanged } from 'generated/proto/event_game_state_changed_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function gameStateChanged(data: Event_GameStateChanged, meta: GameEventMeta): void { +export function gameStateChanged(data: Data.Event_GameStateChanged, meta: Enriched.GameEventMeta): void { GamePersistence.gameStateChanged(meta.gameId, data); } diff --git a/webclient/src/websocket/events/game/index.ts b/webclient/src/websocket/events/game/index.ts index 2e75dabc8..e9931880c 100644 --- a/webclient/src/websocket/events/game/index.ts +++ b/webclient/src/websocket/events/game/index.ts @@ -1,4 +1,7 @@ -import { GameExtensionRegistry, makeGameEntry } from '../../services/protobuf-types'; +import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; + +import { Data, Enriched } from '@app/types'; + import { attachCard } from './attachCard'; import { changeZoneProperties } from './changeZoneProperties'; import { createArrow } from './createArrow'; @@ -29,65 +32,44 @@ import { setCardCounter } from './setCardCounter'; import { setCounter } from './setCounter'; import { shuffle } from './shuffle'; -import { Event_Join_ext } from 'generated/proto/event_join_pb'; -import { Event_Leave_ext } from 'generated/proto/event_leave_pb'; -import { Event_GameClosed_ext } from 'generated/proto/event_game_closed_pb'; -import { Event_GameHostChanged_ext } from 'generated/proto/event_game_host_changed_pb'; -import { Event_Kicked_ext } from 'generated/proto/event_kicked_pb'; -import { Event_GameStateChanged_ext } from 'generated/proto/event_game_state_changed_pb'; -import { Event_PlayerPropertiesChanged_ext } from 'generated/proto/event_player_properties_changed_pb'; -import { Event_GameSay_ext } from 'generated/proto/event_game_say_pb'; -import { Event_CreateArrow_ext } from 'generated/proto/event_create_arrow_pb'; -import { Event_DeleteArrow_ext } from 'generated/proto/event_delete_arrow_pb'; -import { Event_CreateCounter_ext } from 'generated/proto/event_create_counter_pb'; -import { Event_SetCounter_ext } from 'generated/proto/event_set_counter_pb'; -import { Event_DelCounter_ext } from 'generated/proto/event_del_counter_pb'; -import { Event_DrawCards_ext } from 'generated/proto/event_draw_cards_pb'; -import { Event_RevealCards_ext } from 'generated/proto/event_reveal_cards_pb'; -import { Event_Shuffle_ext } from 'generated/proto/event_shuffle_pb'; -import { Event_RollDie_ext } from 'generated/proto/event_roll_die_pb'; -import { Event_MoveCard_ext } from 'generated/proto/event_move_card_pb'; -import { Event_FlipCard_ext } from 'generated/proto/event_flip_card_pb'; -import { Event_DestroyCard_ext } from 'generated/proto/event_destroy_card_pb'; -import { Event_AttachCard_ext } from 'generated/proto/event_attach_card_pb'; -import { Event_CreateToken_ext } from 'generated/proto/event_create_token_pb'; -import { Event_SetCardAttr_ext } from 'generated/proto/event_set_card_attr_pb'; -import { Event_SetCardCounter_ext } from 'generated/proto/event_set_card_counter_pb'; -import { Event_SetActivePlayer_ext } from 'generated/proto/event_set_active_player_pb'; -import { Event_SetActivePhase_ext } from 'generated/proto/event_set_active_phase_pb'; -import { Event_DumpZone_ext } from 'generated/proto/event_dump_zone_pb'; -import { Event_ChangeZoneProperties_ext } from 'generated/proto/event_change_zone_properties_pb'; -import { Event_ReverseTurn_ext } from 'generated/proto/event_reverse_turn_pb'; +type GameRegistryEntry = Data.RegistryEntry; +export type GameExtensionRegistry = GameRegistryEntry[]; + +function makeGameEntry( + ext: GenExtension, + handler: (value: V, meta: Enriched.GameEventMeta) => void, +): GameRegistryEntry { + return Data.makeEntry(ext, handler); +} export const GameEvents: GameExtensionRegistry = [ - makeGameEntry(Event_Join_ext, joinGame), - makeGameEntry(Event_Leave_ext, leaveGame), - makeGameEntry(Event_GameClosed_ext, gameClosed), - makeGameEntry(Event_GameHostChanged_ext, gameHostChanged), - makeGameEntry(Event_Kicked_ext, kicked), - makeGameEntry(Event_GameStateChanged_ext, gameStateChanged), - makeGameEntry(Event_PlayerPropertiesChanged_ext, playerPropertiesChanged), - makeGameEntry(Event_GameSay_ext, gameSay), - makeGameEntry(Event_CreateArrow_ext, createArrow), - makeGameEntry(Event_DeleteArrow_ext, deleteArrow), - makeGameEntry(Event_CreateCounter_ext, createCounter), - makeGameEntry(Event_SetCounter_ext, setCounter), - makeGameEntry(Event_DelCounter_ext, delCounter), - makeGameEntry(Event_DrawCards_ext, drawCards), - makeGameEntry(Event_RevealCards_ext, revealCards), - makeGameEntry(Event_Shuffle_ext, shuffle), - makeGameEntry(Event_RollDie_ext, rollDie), - makeGameEntry(Event_MoveCard_ext, moveCard), - makeGameEntry(Event_FlipCard_ext, flipCard), - makeGameEntry(Event_DestroyCard_ext, destroyCard), - makeGameEntry(Event_AttachCard_ext, attachCard), - makeGameEntry(Event_CreateToken_ext, createToken), - makeGameEntry(Event_SetCardAttr_ext, setCardAttr), - makeGameEntry(Event_SetCardCounter_ext, setCardCounter), - makeGameEntry(Event_SetActivePlayer_ext, setActivePlayer), - makeGameEntry(Event_SetActivePhase_ext, setActivePhase), - makeGameEntry(Event_DumpZone_ext, dumpZone), - makeGameEntry(Event_ChangeZoneProperties_ext, changeZoneProperties), - makeGameEntry(Event_ReverseTurn_ext, reverseTurn), + makeGameEntry(Data.Event_Join_ext, joinGame), + makeGameEntry(Data.Event_Leave_ext, leaveGame), + makeGameEntry(Data.Event_GameClosed_ext, gameClosed), + makeGameEntry(Data.Event_GameHostChanged_ext, gameHostChanged), + makeGameEntry(Data.Event_Kicked_ext, kicked), + makeGameEntry(Data.Event_GameStateChanged_ext, gameStateChanged), + makeGameEntry(Data.Event_PlayerPropertiesChanged_ext, playerPropertiesChanged), + makeGameEntry(Data.Event_GameSay_ext, gameSay), + makeGameEntry(Data.Event_CreateArrow_ext, createArrow), + makeGameEntry(Data.Event_DeleteArrow_ext, deleteArrow), + makeGameEntry(Data.Event_CreateCounter_ext, createCounter), + makeGameEntry(Data.Event_SetCounter_ext, setCounter), + makeGameEntry(Data.Event_DelCounter_ext, delCounter), + makeGameEntry(Data.Event_DrawCards_ext, drawCards), + makeGameEntry(Data.Event_RevealCards_ext, revealCards), + makeGameEntry(Data.Event_Shuffle_ext, shuffle), + makeGameEntry(Data.Event_RollDie_ext, rollDie), + makeGameEntry(Data.Event_MoveCard_ext, moveCard), + makeGameEntry(Data.Event_FlipCard_ext, flipCard), + makeGameEntry(Data.Event_DestroyCard_ext, destroyCard), + makeGameEntry(Data.Event_AttachCard_ext, attachCard), + makeGameEntry(Data.Event_CreateToken_ext, createToken), + makeGameEntry(Data.Event_SetCardAttr_ext, setCardAttr), + makeGameEntry(Data.Event_SetCardCounter_ext, setCardCounter), + makeGameEntry(Data.Event_SetActivePlayer_ext, setActivePlayer), + makeGameEntry(Data.Event_SetActivePhase_ext, setActivePhase), + makeGameEntry(Data.Event_DumpZone_ext, dumpZone), + makeGameEntry(Data.Event_ChangeZoneProperties_ext, changeZoneProperties), + makeGameEntry(Data.Event_ReverseTurn_ext, reverseTurn), ]; - diff --git a/webclient/src/websocket/events/game/joinGame.ts b/webclient/src/websocket/events/game/joinGame.ts index 640e83e96..376801f73 100644 --- a/webclient/src/websocket/events/game/joinGame.ts +++ b/webclient/src/websocket/events/game/joinGame.ts @@ -1,7 +1,6 @@ import { GamePersistence } from '../../persistence'; -import type { ServerInfo_PlayerProperties } from 'generated/proto/serverinfo_playerproperties_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; -export function joinGame(data: { playerProperties: ServerInfo_PlayerProperties }, meta: GameEventMeta): void { +export function joinGame(data: { playerProperties: Data.ServerInfo_PlayerProperties }, meta: Enriched.GameEventMeta): void { GamePersistence.playerJoined(meta.gameId, data.playerProperties); } diff --git a/webclient/src/websocket/events/game/kicked.ts b/webclient/src/websocket/events/game/kicked.ts index 7bf94d5ad..a409cb895 100644 --- a/webclient/src/websocket/events/game/kicked.ts +++ b/webclient/src/websocket/events/game/kicked.ts @@ -1,6 +1,6 @@ -import { GameEventMeta } from 'types'; +import { Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function kicked(_data: {}, meta: GameEventMeta): void { +export function kicked(_data: {}, meta: Enriched.GameEventMeta): void { GamePersistence.kicked(meta.gameId); } diff --git a/webclient/src/websocket/events/game/leaveGame.ts b/webclient/src/websocket/events/game/leaveGame.ts index 9367ba5bb..703fcd86b 100644 --- a/webclient/src/websocket/events/game/leaveGame.ts +++ b/webclient/src/websocket/events/game/leaveGame.ts @@ -1,6 +1,6 @@ -import { GameEventMeta } from 'types'; +import { Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function leaveGame(data: { reason: number }, meta: GameEventMeta): void { +export function leaveGame(data: { reason: number }, meta: Enriched.GameEventMeta): void { GamePersistence.playerLeft(meta.gameId, meta.playerId, data.reason ?? 1); } diff --git a/webclient/src/websocket/events/game/moveCard.ts b/webclient/src/websocket/events/game/moveCard.ts index a15666d57..01e33d570 100644 --- a/webclient/src/websocket/events/game/moveCard.ts +++ b/webclient/src/websocket/events/game/moveCard.ts @@ -1,7 +1,6 @@ -import type { Event_MoveCard } from 'generated/proto/event_move_card_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function moveCard(data: Event_MoveCard, meta: GameEventMeta): void { +export function moveCard(data: Data.Event_MoveCard, meta: Enriched.GameEventMeta): void { GamePersistence.cardMoved(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/playerPropertiesChanged.ts b/webclient/src/websocket/events/game/playerPropertiesChanged.ts index 0dbe440ed..dcc0acef6 100644 --- a/webclient/src/websocket/events/game/playerPropertiesChanged.ts +++ b/webclient/src/websocket/events/game/playerPropertiesChanged.ts @@ -1,7 +1,6 @@ -import type { ServerInfo_PlayerProperties } from 'generated/proto/serverinfo_playerproperties_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function playerPropertiesChanged(data: { playerProperties: ServerInfo_PlayerProperties }, meta: GameEventMeta): void { +export function playerPropertiesChanged(data: { playerProperties: Data.ServerInfo_PlayerProperties }, meta: Enriched.GameEventMeta): void { GamePersistence.playerPropertiesChanged(meta.gameId, meta.playerId, data.playerProperties); } diff --git a/webclient/src/websocket/events/game/revealCards.ts b/webclient/src/websocket/events/game/revealCards.ts index 3837786f5..42ab882ff 100644 --- a/webclient/src/websocket/events/game/revealCards.ts +++ b/webclient/src/websocket/events/game/revealCards.ts @@ -1,7 +1,6 @@ -import type { Event_RevealCards } from 'generated/proto/event_reveal_cards_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function revealCards(data: Event_RevealCards, meta: GameEventMeta): void { +export function revealCards(data: Data.Event_RevealCards, meta: Enriched.GameEventMeta): void { GamePersistence.cardsRevealed(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/reverseTurn.ts b/webclient/src/websocket/events/game/reverseTurn.ts index 8716612e3..ae7234d8f 100644 --- a/webclient/src/websocket/events/game/reverseTurn.ts +++ b/webclient/src/websocket/events/game/reverseTurn.ts @@ -1,7 +1,6 @@ -import type { Event_ReverseTurn } from 'generated/proto/event_reverse_turn_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function reverseTurn(data: Event_ReverseTurn, meta: GameEventMeta): void { +export function reverseTurn(data: Data.Event_ReverseTurn, meta: Enriched.GameEventMeta): void { GamePersistence.turnReversed(meta.gameId, data.reversed); } diff --git a/webclient/src/websocket/events/game/rollDie.ts b/webclient/src/websocket/events/game/rollDie.ts index 4da34387d..2c262bb8b 100644 --- a/webclient/src/websocket/events/game/rollDie.ts +++ b/webclient/src/websocket/events/game/rollDie.ts @@ -1,7 +1,6 @@ -import type { Event_RollDie } from 'generated/proto/event_roll_die_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function rollDie(data: Event_RollDie, meta: GameEventMeta): void { +export function rollDie(data: Data.Event_RollDie, meta: Enriched.GameEventMeta): void { GamePersistence.dieRolled(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/setActivePhase.ts b/webclient/src/websocket/events/game/setActivePhase.ts index 5981b97bc..2a1f1ef3d 100644 --- a/webclient/src/websocket/events/game/setActivePhase.ts +++ b/webclient/src/websocket/events/game/setActivePhase.ts @@ -1,7 +1,6 @@ -import type { Event_SetActivePhase } from 'generated/proto/event_set_active_phase_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function setActivePhase(data: Event_SetActivePhase, meta: GameEventMeta): void { +export function setActivePhase(data: Data.Event_SetActivePhase, meta: Enriched.GameEventMeta): void { GamePersistence.activePhaseSet(meta.gameId, data.phase); } diff --git a/webclient/src/websocket/events/game/setActivePlayer.ts b/webclient/src/websocket/events/game/setActivePlayer.ts index 0f529b6e4..7966fe7e5 100644 --- a/webclient/src/websocket/events/game/setActivePlayer.ts +++ b/webclient/src/websocket/events/game/setActivePlayer.ts @@ -1,7 +1,6 @@ -import type { Event_SetActivePlayer } from 'generated/proto/event_set_active_player_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function setActivePlayer(data: Event_SetActivePlayer, meta: GameEventMeta): void { +export function setActivePlayer(data: Data.Event_SetActivePlayer, meta: Enriched.GameEventMeta): void { GamePersistence.activePlayerSet(meta.gameId, data.activePlayerId); } diff --git a/webclient/src/websocket/events/game/setCardAttr.ts b/webclient/src/websocket/events/game/setCardAttr.ts index aa15043fb..688b0d1f2 100644 --- a/webclient/src/websocket/events/game/setCardAttr.ts +++ b/webclient/src/websocket/events/game/setCardAttr.ts @@ -1,7 +1,6 @@ -import type { Event_SetCardAttr } from 'generated/proto/event_set_card_attr_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function setCardAttr(data: Event_SetCardAttr, meta: GameEventMeta): void { +export function setCardAttr(data: Data.Event_SetCardAttr, meta: Enriched.GameEventMeta): void { GamePersistence.cardAttrChanged(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/setCardCounter.ts b/webclient/src/websocket/events/game/setCardCounter.ts index e595b65a7..19e4ff236 100644 --- a/webclient/src/websocket/events/game/setCardCounter.ts +++ b/webclient/src/websocket/events/game/setCardCounter.ts @@ -1,7 +1,6 @@ -import type { Event_SetCardCounter } from 'generated/proto/event_set_card_counter_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function setCardCounter(data: Event_SetCardCounter, meta: GameEventMeta): void { +export function setCardCounter(data: Data.Event_SetCardCounter, meta: Enriched.GameEventMeta): void { GamePersistence.cardCounterChanged(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/setCounter.ts b/webclient/src/websocket/events/game/setCounter.ts index a7e6863ae..3de940761 100644 --- a/webclient/src/websocket/events/game/setCounter.ts +++ b/webclient/src/websocket/events/game/setCounter.ts @@ -1,7 +1,6 @@ -import type { Event_SetCounter } from 'generated/proto/event_set_counter_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function setCounter(data: Event_SetCounter, meta: GameEventMeta): void { +export function setCounter(data: Data.Event_SetCounter, meta: Enriched.GameEventMeta): void { GamePersistence.counterSet(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/game/shuffle.ts b/webclient/src/websocket/events/game/shuffle.ts index a1ec8ba05..8b242e38f 100644 --- a/webclient/src/websocket/events/game/shuffle.ts +++ b/webclient/src/websocket/events/game/shuffle.ts @@ -1,7 +1,6 @@ -import type { Event_Shuffle } from 'generated/proto/event_shuffle_pb'; -import type { GameEventMeta } from 'types'; +import type { Data, Enriched } from '@app/types'; import { GamePersistence } from '../../persistence'; -export function shuffle(data: Event_Shuffle, meta: GameEventMeta): void { +export function shuffle(data: Data.Event_Shuffle, meta: Enriched.GameEventMeta): void { GamePersistence.zoneShuffled(meta.gameId, meta.playerId, data); } diff --git a/webclient/src/websocket/events/room/index.ts b/webclient/src/websocket/events/room/index.ts index f1832faa9..c02805659 100644 --- a/webclient/src/websocket/events/room/index.ts +++ b/webclient/src/websocket/events/room/index.ts @@ -1,4 +1,6 @@ -import { RoomExtensionRegistry, makeRoomEntry } from '../../services/protobuf-types'; +import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; + +import { Data } from '@app/types'; import { joinRoom } from './joinRoom'; import { leaveRoom } from './leaveRoom'; @@ -6,17 +8,20 @@ import { listGames } from './listGames'; import { roomSay } from './roomSay'; import { removeMessages } from './removeMessages'; -import { Event_JoinRoom_ext } from 'generated/proto/event_join_room_pb'; -import { Event_LeaveRoom_ext } from 'generated/proto/event_leave_room_pb'; -import { Event_ListGames_ext } from 'generated/proto/event_list_games_pb'; -import { Event_RemoveMessages_ext } from 'generated/proto/event_remove_messages_pb'; -import { Event_RoomSay_ext } from 'generated/proto/event_room_say_pb'; +type RoomRegistryEntry = Data.RegistryEntry; +export type RoomExtensionRegistry = RoomRegistryEntry[]; + +function makeRoomEntry( + ext: GenExtension, + handler: (value: V, roomEvent: Data.RoomEvent) => void, +): RoomRegistryEntry { + return Data.makeEntry(ext, handler); +} export const RoomEvents: RoomExtensionRegistry = [ - makeRoomEntry(Event_JoinRoom_ext, joinRoom), - makeRoomEntry(Event_LeaveRoom_ext, leaveRoom), - makeRoomEntry(Event_ListGames_ext, listGames), - makeRoomEntry(Event_RemoveMessages_ext, removeMessages), - makeRoomEntry(Event_RoomSay_ext, roomSay), + makeRoomEntry(Data.Event_JoinRoom_ext, joinRoom), + makeRoomEntry(Data.Event_LeaveRoom_ext, leaveRoom), + makeRoomEntry(Data.Event_ListGames_ext, listGames), + makeRoomEntry(Data.Event_RemoveMessages_ext, removeMessages), + makeRoomEntry(Data.Event_RoomSay_ext, roomSay), ]; - diff --git a/webclient/src/websocket/events/room/interfaces.ts b/webclient/src/websocket/events/room/interfaces.ts deleted file mode 100644 index 6e8a61b01..000000000 --- a/webclient/src/websocket/events/room/interfaces.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { Event_JoinRoom } from 'generated/proto/event_join_room_pb'; -import type { Event_LeaveRoom } from 'generated/proto/event_leave_room_pb'; -import type { Event_ListGames } from 'generated/proto/event_list_games_pb'; -import type { Event_RemoveMessages } from 'generated/proto/event_remove_messages_pb'; -import type { Event_RoomSay } from 'generated/proto/event_room_say_pb'; -import type { RoomEvent as GeneratedRoomEvent } from 'generated/proto/room_event_pb'; - -export type JoinRoomData = Event_JoinRoom; -export type LeaveRoomData = Event_LeaveRoom; -export type ListGamesData = Event_ListGames; -export type RemoveMessagesData = Event_RemoveMessages; -export type RoomSayData = Event_RoomSay; -export type RoomEvent = GeneratedRoomEvent; diff --git a/webclient/src/websocket/events/room/joinRoom.ts b/webclient/src/websocket/events/room/joinRoom.ts index f45ff18ab..1dab289d7 100644 --- a/webclient/src/websocket/events/room/joinRoom.ts +++ b/webclient/src/websocket/events/room/joinRoom.ts @@ -1,6 +1,6 @@ +import type { Data } from '@app/types'; import { RoomPersistence } from '../../persistence'; -import { JoinRoomData, RoomEvent } from './interfaces'; -export function joinRoom({ userInfo }: JoinRoomData, { roomId }: RoomEvent): void { +export function joinRoom({ userInfo }: Data.Event_JoinRoom, { roomId }: Data.RoomEvent): void { RoomPersistence.userJoined(roomId, userInfo); } diff --git a/webclient/src/websocket/events/room/leaveRoom.ts b/webclient/src/websocket/events/room/leaveRoom.ts index c7564d458..df69c783f 100644 --- a/webclient/src/websocket/events/room/leaveRoom.ts +++ b/webclient/src/websocket/events/room/leaveRoom.ts @@ -1,6 +1,6 @@ +import type { Data } from '@app/types'; import { RoomPersistence } from '../../persistence'; -import { LeaveRoomData, RoomEvent } from './interfaces'; -export function leaveRoom({ name }: LeaveRoomData, { roomId }: RoomEvent): void { +export function leaveRoom({ name }: Data.Event_LeaveRoom, { roomId }: Data.RoomEvent): void { RoomPersistence.userLeft(roomId, name); } diff --git a/webclient/src/websocket/events/room/listGames.ts b/webclient/src/websocket/events/room/listGames.ts index 0f1f20438..a943fed41 100644 --- a/webclient/src/websocket/events/room/listGames.ts +++ b/webclient/src/websocket/events/room/listGames.ts @@ -1,6 +1,6 @@ +import type { Data } from '@app/types'; import { RoomPersistence } from '../../persistence'; -import { ListGamesData, RoomEvent } from './interfaces'; -export function listGames({ gameList }: ListGamesData, { roomId }: RoomEvent): void { +export function listGames({ gameList }: Data.Event_ListGames, { roomId }: Data.RoomEvent): void { RoomPersistence.updateGames(roomId, gameList); } diff --git a/webclient/src/websocket/events/room/removeMessages.ts b/webclient/src/websocket/events/room/removeMessages.ts index 859470c81..b6342e50d 100644 --- a/webclient/src/websocket/events/room/removeMessages.ts +++ b/webclient/src/websocket/events/room/removeMessages.ts @@ -1,6 +1,6 @@ +import type { Data } from '@app/types'; import { RoomPersistence } from '../../persistence'; -import { RemoveMessagesData, RoomEvent } from './interfaces'; -export function removeMessages({ name, amount }: RemoveMessagesData, { roomId }: RoomEvent): void { +export function removeMessages({ name, amount }: Data.Event_RemoveMessages, { roomId }: Data.RoomEvent): void { RoomPersistence.removeMessages(roomId, name, amount); } diff --git a/webclient/src/websocket/events/room/roomEvents.spec.ts b/webclient/src/websocket/events/room/roomEvents.spec.ts index 60690d487..60d8fe765 100644 --- a/webclient/src/websocket/events/room/roomEvents.spec.ts +++ b/webclient/src/websocket/events/room/roomEvents.spec.ts @@ -9,27 +9,20 @@ vi.mock('../../persistence', () => ({ })); import { create } from '@bufbuild/protobuf'; +import { Data } from '@app/types'; import { RoomPersistence } from '../../persistence'; import { joinRoom } from './joinRoom'; import { leaveRoom } from './leaveRoom'; import { listGames } from './listGames'; import { removeMessages } from './removeMessages'; import { roomSay } from './roomSay'; -import { Event_JoinRoomSchema } from 'generated/proto/event_join_room_pb'; -import { Event_LeaveRoomSchema } from 'generated/proto/event_leave_room_pb'; -import { Event_ListGamesSchema } from 'generated/proto/event_list_games_pb'; -import { Event_RemoveMessagesSchema } from 'generated/proto/event_remove_messages_pb'; -import { Event_RoomSaySchema } from 'generated/proto/event_room_say_pb'; -import { RoomEventSchema } from 'generated/proto/room_event_pb'; -const makeRoomEvent = (roomId: number) => create(RoomEventSchema, { roomId }); - -beforeEach(() => vi.clearAllMocks()); +const makeRoomEvent = (roomId: number) => create(Data.RoomEventSchema, { roomId }); describe('joinRoom room event', () => { it('calls RoomPersistence.userJoined with roomId and userInfo', () => { - const data = create(Event_JoinRoomSchema, { userInfo: { name: 'alice' } }); + const data = create(Data.Event_JoinRoomSchema, { userInfo: { name: 'alice' } }); joinRoom(data, makeRoomEvent(3)); expect(RoomPersistence.userJoined).toHaveBeenCalledWith(3, data.userInfo); }); @@ -38,7 +31,7 @@ describe('joinRoom room event', () => { describe('leaveRoom room event', () => { it('calls RoomPersistence.userLeft with roomId and name', () => { - leaveRoom(create(Event_LeaveRoomSchema, { name: 'alice' }), makeRoomEvent(4)); + leaveRoom(create(Data.Event_LeaveRoomSchema, { name: 'alice' }), makeRoomEvent(4)); expect(RoomPersistence.userLeft).toHaveBeenCalledWith(4, 'alice'); }); }); @@ -46,7 +39,7 @@ describe('leaveRoom room event', () => { describe('listGames room event', () => { it('calls RoomPersistence.updateGames with roomId and gameList', () => { - const data = create(Event_ListGamesSchema, { gameList: [{ gameId: 1 }] }); + const data = create(Data.Event_ListGamesSchema, { gameList: [{ gameId: 1 }] }); listGames(data, makeRoomEvent(5)); expect(RoomPersistence.updateGames).toHaveBeenCalledWith(5, data.gameList); }); @@ -55,7 +48,7 @@ describe('listGames room event', () => { describe('removeMessages room event', () => { it('calls RoomPersistence.removeMessages with roomId, name, amount', () => { - removeMessages(create(Event_RemoveMessagesSchema, { name: 'bob', amount: 10 }), makeRoomEvent(6)); + removeMessages(create(Data.Event_RemoveMessagesSchema, { name: 'bob', amount: 10 }), makeRoomEvent(6)); expect(RoomPersistence.removeMessages).toHaveBeenCalledWith(6, 'bob', 10); }); }); @@ -67,7 +60,7 @@ describe('roomSay room event', () => { afterEach(() => vi.useRealTimers()); it('calls RoomPersistence.addMessage with roomId and message', () => { - const data = create(Event_RoomSaySchema, { message: 'hello' }); + const data = create(Data.Event_RoomSaySchema, { message: 'hello' }); roomSay(data, makeRoomEvent(7)); expect(RoomPersistence.addMessage).toHaveBeenCalledWith(7, { ...data, timeReceived: 0 }); }); diff --git a/webclient/src/websocket/events/room/roomSay.ts b/webclient/src/websocket/events/room/roomSay.ts index f6accd8a9..25b35b12b 100644 --- a/webclient/src/websocket/events/room/roomSay.ts +++ b/webclient/src/websocket/events/room/roomSay.ts @@ -1,9 +1,9 @@ -import { Message } from 'types'; +import type { Data } from '@app/types'; +import { Enriched } from '@app/types'; import { RoomPersistence } from '../../persistence'; -import { RoomSayData, RoomEvent } from './interfaces'; -export function roomSay(data: RoomSayData, { roomId }: RoomEvent): void { - const message: Message = { ...data, timeReceived: Date.now() }; +export function roomSay(data: Data.Event_RoomSay, { roomId }: Data.RoomEvent): void { + const message: Enriched.Message = { ...data, timeReceived: Date.now() }; RoomPersistence.addMessage(roomId, message); } diff --git a/webclient/src/websocket/events/session/addToList.ts b/webclient/src/websocket/events/session/addToList.ts index 08b19b45d..3c820037a 100644 --- a/webclient/src/websocket/events/session/addToList.ts +++ b/webclient/src/websocket/events/session/addToList.ts @@ -1,7 +1,7 @@ +import type { Data } from '@app/types'; import { SessionPersistence } from '../../persistence'; -import { AddToListData } from './interfaces'; -export function addToList({ listName, userInfo }: AddToListData): void { +export function addToList({ listName, userInfo }: Data.Event_AddToList): void { switch (listName) { case 'buddy': { SessionPersistence.addToBuddyList(userInfo); diff --git a/webclient/src/websocket/events/session/connectionClosed.ts b/webclient/src/websocket/events/session/connectionClosed.ts index c98080f5f..fcde2fceb 100644 --- a/webclient/src/websocket/events/session/connectionClosed.ts +++ b/webclient/src/websocket/events/session/connectionClosed.ts @@ -1,9 +1,7 @@ -import { StatusEnum } from 'types'; -import { Event_ConnectionClosed_CloseReason } from 'generated/proto/event_connection_closed_pb'; +import { App, Data } from '@app/types'; import { updateStatus } from '../../commands/session'; -import { ConnectionClosedData } from './interfaces'; -export function connectionClosed({ reason, reasonStr, endTime }: ConnectionClosedData): void { +export function connectionClosed({ reason, reasonStr, endTime }: Data.Event_ConnectionClosed): void { let message: string; // @TODO (5) @@ -11,35 +9,35 @@ export function connectionClosed({ reason, reasonStr, endTime }: ConnectionClose message = reasonStr; } else { switch (reason) { - case Event_ConnectionClosed_CloseReason.USER_LIMIT_REACHED: + case Data.Event_ConnectionClosed_CloseReason.USER_LIMIT_REACHED: message = 'The server has reached its maximum user capacity'; break; - case Event_ConnectionClosed_CloseReason.TOO_MANY_CONNECTIONS: + case Data.Event_ConnectionClosed_CloseReason.TOO_MANY_CONNECTIONS: message = 'There are too many concurrent connections from your address'; break; - case Event_ConnectionClosed_CloseReason.BANNED: + case Data.Event_ConnectionClosed_CloseReason.BANNED: message = typeof endTime === 'number' && endTime > 0 && Number.isFinite(endTime) ? `You are banned until ${new Date(endTime * 1000).toLocaleString()}` : 'You are banned'; break; - case Event_ConnectionClosed_CloseReason.DEMOTED: + case Data.Event_ConnectionClosed_CloseReason.DEMOTED: message = 'You were demoted'; break; - case Event_ConnectionClosed_CloseReason.SERVER_SHUTDOWN: + case Data.Event_ConnectionClosed_CloseReason.SERVER_SHUTDOWN: message = 'Scheduled server shutdown'; break; - case Event_ConnectionClosed_CloseReason.USERNAMEINVALID: + case Data.Event_ConnectionClosed_CloseReason.USERNAMEINVALID: message = 'Invalid username'; break; - case Event_ConnectionClosed_CloseReason.LOGGEDINELSEWERE: + case Data.Event_ConnectionClosed_CloseReason.LOGGEDINELSEWERE: message = 'You have been logged out due to logging in at another location'; break; - case Event_ConnectionClosed_CloseReason.OTHER: + case Data.Event_ConnectionClosed_CloseReason.OTHER: default: message = 'Unknown reason'; break; } } - updateStatus(StatusEnum.DISCONNECTED, message); + updateStatus(App.StatusEnum.DISCONNECTED, message); } diff --git a/webclient/src/websocket/events/session/gameJoined.ts b/webclient/src/websocket/events/session/gameJoined.ts index 8c0d49006..4988039dd 100644 --- a/webclient/src/websocket/events/session/gameJoined.ts +++ b/webclient/src/websocket/events/session/gameJoined.ts @@ -1,6 +1,6 @@ +import type { Data } from '@app/types'; import { SessionPersistence } from '../../persistence'; -import { GameJoinedData } from './interfaces'; -export function gameJoined(gameJoined: GameJoinedData): void { +export function gameJoined(gameJoined: Data.Event_GameJoined): void { SessionPersistence.gameJoined(gameJoined); } diff --git a/webclient/src/websocket/events/session/index.ts b/webclient/src/websocket/events/session/index.ts index d2b8cc4e9..3d5935c9f 100644 --- a/webclient/src/websocket/events/session/index.ts +++ b/webclient/src/websocket/events/session/index.ts @@ -1,4 +1,7 @@ -import { SessionExtensionRegistry, makeSessionEntry } from '../../services/protobuf-types'; +import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; + +import { Data } from '@app/types'; + import { addToList } from './addToList'; import { connectionClosed } from './connectionClosed'; import { listRooms } from './listRooms'; @@ -14,35 +17,29 @@ import { userLeft } from './userLeft'; import { userMessage } from './userMessage'; import { gameJoined } from './gameJoined'; -import { Event_AddToList_ext } from 'generated/proto/event_add_to_list_pb'; -import { Event_ConnectionClosed_ext } from 'generated/proto/event_connection_closed_pb'; -import { Event_GameJoined_ext } from 'generated/proto/event_game_joined_pb'; -import { Event_ListRooms_ext } from 'generated/proto/event_list_rooms_pb'; -import { Event_NotifyUser_ext } from 'generated/proto/event_notify_user_pb'; -import { Event_RemoveFromList_ext } from 'generated/proto/event_remove_from_list_pb'; -import { Event_ReplayAdded_ext } from 'generated/proto/event_replay_added_pb'; -import { Event_ServerCompleteList_ext } from 'generated/proto/event_server_complete_list_pb'; -import { Event_ServerIdentification_ext } from 'generated/proto/event_server_identification_pb'; -import { Event_ServerMessage_ext } from 'generated/proto/event_server_message_pb'; -import { Event_ServerShutdown_ext } from 'generated/proto/event_server_shutdown_pb'; -import { Event_UserJoined_ext } from 'generated/proto/event_user_joined_pb'; -import { Event_UserLeft_ext } from 'generated/proto/event_user_left_pb'; -import { Event_UserMessage_ext } from 'generated/proto/event_user_message_pb'; +type SessionRegistryEntry = Data.RegistryEntry; +export type SessionExtensionRegistry = SessionRegistryEntry[]; + +function makeSessionEntry( + ext: GenExtension, + handler: (value: V) => void, +): SessionRegistryEntry { + return Data.makeEntry(ext, handler); +} export const SessionEvents: SessionExtensionRegistry = [ - makeSessionEntry(Event_AddToList_ext, addToList), - makeSessionEntry(Event_ConnectionClosed_ext, connectionClosed), - makeSessionEntry(Event_GameJoined_ext, gameJoined), - makeSessionEntry(Event_ListRooms_ext, listRooms), - makeSessionEntry(Event_NotifyUser_ext, notifyUser), - makeSessionEntry(Event_RemoveFromList_ext, removeFromList), - makeSessionEntry(Event_ReplayAdded_ext, replayAdded), - makeSessionEntry(Event_ServerCompleteList_ext, serverCompleteList), - makeSessionEntry(Event_ServerIdentification_ext, serverIdentification), - makeSessionEntry(Event_ServerMessage_ext, serverMessage), - makeSessionEntry(Event_ServerShutdown_ext, serverShutdown), - makeSessionEntry(Event_UserJoined_ext, userJoined), - makeSessionEntry(Event_UserLeft_ext, userLeft), - makeSessionEntry(Event_UserMessage_ext, userMessage), + makeSessionEntry(Data.Event_AddToList_ext, addToList), + makeSessionEntry(Data.Event_ConnectionClosed_ext, connectionClosed), + makeSessionEntry(Data.Event_GameJoined_ext, gameJoined), + makeSessionEntry(Data.Event_ListRooms_ext, listRooms), + makeSessionEntry(Data.Event_NotifyUser_ext, notifyUser), + makeSessionEntry(Data.Event_RemoveFromList_ext, removeFromList), + makeSessionEntry(Data.Event_ReplayAdded_ext, replayAdded), + makeSessionEntry(Data.Event_ServerCompleteList_ext, serverCompleteList), + makeSessionEntry(Data.Event_ServerIdentification_ext, serverIdentification), + makeSessionEntry(Data.Event_ServerMessage_ext, serverMessage), + makeSessionEntry(Data.Event_ServerShutdown_ext, serverShutdown), + makeSessionEntry(Data.Event_UserJoined_ext, userJoined), + makeSessionEntry(Data.Event_UserLeft_ext, userLeft), + makeSessionEntry(Data.Event_UserMessage_ext, userMessage), ]; - diff --git a/webclient/src/websocket/events/session/interfaces.ts b/webclient/src/websocket/events/session/interfaces.ts deleted file mode 100644 index fcd1fd1b7..000000000 --- a/webclient/src/websocket/events/session/interfaces.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { Event_AddToList } from 'generated/proto/event_add_to_list_pb'; -import type { Event_ConnectionClosed } from 'generated/proto/event_connection_closed_pb'; -import type { Event_GameJoined } from 'generated/proto/event_game_joined_pb'; -import type { Event_ListRooms } from 'generated/proto/event_list_rooms_pb'; -import type { Event_NotifyUser } from 'generated/proto/event_notify_user_pb'; -import type { Event_RemoveFromList } from 'generated/proto/event_remove_from_list_pb'; -import type { Event_ReplayAdded } from 'generated/proto/event_replay_added_pb'; -import type { Event_ServerCompleteList } from 'generated/proto/event_server_complete_list_pb'; -import type { Event_ServerIdentification } from 'generated/proto/event_server_identification_pb'; -import type { Event_ServerMessage } from 'generated/proto/event_server_message_pb'; -import type { Event_ServerShutdown } from 'generated/proto/event_server_shutdown_pb'; -import type { Event_UserJoined } from 'generated/proto/event_user_joined_pb'; -import type { Event_UserLeft } from 'generated/proto/event_user_left_pb'; -import type { Event_UserMessage } from 'generated/proto/event_user_message_pb'; -import type { Event_PlayerPropertiesChanged } from 'generated/proto/event_player_properties_changed_pb'; - -export type AddToListData = Event_AddToList; -export type ConnectionClosedData = Event_ConnectionClosed; -export type GameJoinedData = Event_GameJoined; -export type ListRoomsData = Event_ListRooms; -export type NotifyUserData = Event_NotifyUser; -export type RemoveFromListData = Event_RemoveFromList; -export type ReplayAddedData = Event_ReplayAdded; -export type ServerCompleteListData = Event_ServerCompleteList; -export type ServerIdentificationData = Event_ServerIdentification; -export type ServerMessageData = Event_ServerMessage; -export type ServerShutdownData = Event_ServerShutdown; -export type UserJoinedData = Event_UserJoined; -export type UserLeftData = Event_UserLeft; -export type UserMessageData = Event_UserMessage; -export type PlayerGamePropertiesData = Event_PlayerPropertiesChanged; diff --git a/webclient/src/websocket/events/session/listRooms.ts b/webclient/src/websocket/events/session/listRooms.ts index 697bdaed3..2b94ea01a 100644 --- a/webclient/src/websocket/events/session/listRooms.ts +++ b/webclient/src/websocket/events/session/listRooms.ts @@ -1,9 +1,9 @@ +import type { Data } from '@app/types'; import { CLIENT_OPTIONS } from '../../config'; import { joinRoom } from '../../commands/session'; import { RoomPersistence } from '../../persistence'; -import { ListRoomsData } from './interfaces'; -export function listRooms({ roomList }: ListRoomsData): void { +export function listRooms({ roomList }: Data.Event_ListRooms): void { RoomPersistence.updateRooms(roomList); if (CLIENT_OPTIONS.autojoinrooms) { diff --git a/webclient/src/websocket/events/session/notifyUser.ts b/webclient/src/websocket/events/session/notifyUser.ts index f5673fe3f..438f82264 100644 --- a/webclient/src/websocket/events/session/notifyUser.ts +++ b/webclient/src/websocket/events/session/notifyUser.ts @@ -1,7 +1,6 @@ +import type { Data } from '@app/types'; import { SessionPersistence } from '../../persistence'; -import { NotifyUserData } from './interfaces'; - -export function notifyUser(payload: NotifyUserData): void { +export function notifyUser(payload: Data.Event_NotifyUser): void { SessionPersistence.notifyUser(payload); } diff --git a/webclient/src/websocket/events/session/removeFromList.ts b/webclient/src/websocket/events/session/removeFromList.ts index 20e2d7f54..776ac7c6d 100644 --- a/webclient/src/websocket/events/session/removeFromList.ts +++ b/webclient/src/websocket/events/session/removeFromList.ts @@ -1,7 +1,7 @@ +import type { Data } from '@app/types'; import { SessionPersistence } from '../../persistence'; -import { RemoveFromListData } from './interfaces'; -export function removeFromList({ listName, userName }: RemoveFromListData): void { +export function removeFromList({ listName, userName }: Data.Event_RemoveFromList): void { switch (listName) { case 'buddy': { SessionPersistence.removeFromBuddyList(userName); diff --git a/webclient/src/websocket/events/session/replayAdded.ts b/webclient/src/websocket/events/session/replayAdded.ts index 18a4ea82d..eacf01e86 100644 --- a/webclient/src/websocket/events/session/replayAdded.ts +++ b/webclient/src/websocket/events/session/replayAdded.ts @@ -1,6 +1,6 @@ +import type { Data } from '@app/types'; import { SessionPersistence } from '../../persistence'; -import { ReplayAddedData } from './interfaces'; -export function replayAdded({ matchInfo }: ReplayAddedData): void { +export function replayAdded({ matchInfo }: Data.Event_ReplayAdded): void { SessionPersistence.replayAdded(matchInfo); } diff --git a/webclient/src/websocket/events/session/serverCompleteList.ts b/webclient/src/websocket/events/session/serverCompleteList.ts index 77d37a31e..4d131128c 100644 --- a/webclient/src/websocket/events/session/serverCompleteList.ts +++ b/webclient/src/websocket/events/session/serverCompleteList.ts @@ -1,7 +1,7 @@ +import type { Data } from '@app/types'; import { RoomPersistence, SessionPersistence } from '../../persistence'; -import { ServerCompleteListData } from './interfaces'; -export function serverCompleteList({ userList, roomList }: ServerCompleteListData): void { +export function serverCompleteList({ userList, roomList }: Data.Event_ServerCompleteList): void { SessionPersistence.updateUsers(userList); RoomPersistence.updateRooms(roomList); } diff --git a/webclient/src/websocket/events/session/serverIdentification.ts b/webclient/src/websocket/events/session/serverIdentification.ts index d5998d3d3..d8e7f813d 100644 --- a/webclient/src/websocket/events/session/serverIdentification.ts +++ b/webclient/src/websocket/events/session/serverIdentification.ts @@ -1,4 +1,4 @@ -import { StatusEnum, WebSocketConnectOptions, WebSocketConnectReason } from 'types'; +import { App, Data, Enriched } from '@app/types'; import webClient from '../../WebClient'; import { PROTOCOL_VERSION } from '../../config'; @@ -14,59 +14,77 @@ import { updateStatus, } from '../../commands/session'; import { generateSalt, passwordSaltSupported } from '../../utils'; -import { ServerIdentificationData } from './interfaces'; import { SessionPersistence } from '../../persistence'; -export function serverIdentification(info: ServerIdentificationData): void { +export function serverIdentification(info: Data.Event_ServerIdentification): void { const { serverName, serverVersion, protocolVersion, serverOptions } = info; if (protocolVersion !== PROTOCOL_VERSION) { - updateStatus(StatusEnum.DISCONNECTED, `Protocol version mismatch: ${protocolVersion}`); + updateStatus(App.StatusEnum.DISCONNECTED, `Protocol version mismatch: ${protocolVersion}`); disconnect(); return; } const getPasswordSalt = passwordSaltSupported(serverOptions); - const { password, newPassword, ...connectOptions } = webClient.options; + const options = webClient.options; - switch (connectOptions.reason) { - case WebSocketConnectReason.LOGIN: - updateStatus(StatusEnum.LOGGING_IN, 'Logging In...'); - if (getPasswordSalt) { - requestPasswordSalt(connectOptions, password); - } else { - login(connectOptions, password); - } - break; - case WebSocketConnectReason.REGISTER: - const passwordSalt = getPasswordSalt ? generateSalt() : null; - register(connectOptions, password, passwordSalt); - break; - case WebSocketConnectReason.ACTIVATE_ACCOUNT: - if (getPasswordSalt) { - requestPasswordSalt(connectOptions, password); - } else { - activate(connectOptions, password); - } - break; - case WebSocketConnectReason.PASSWORD_RESET_REQUEST: - forgotPasswordRequest(connectOptions); - break; - case WebSocketConnectReason.PASSWORD_RESET_CHALLENGE: - forgotPasswordChallenge(connectOptions); - break; - case WebSocketConnectReason.PASSWORD_RESET: - if (getPasswordSalt) { - requestPasswordSalt(connectOptions, undefined, newPassword); - } else { - forgotPasswordReset(connectOptions, newPassword); - } - break; - default: - updateStatus(StatusEnum.DISCONNECTED, 'Unknown Connection Reason: ' + connectOptions.reason); - disconnect(); - break; + if (!options) { + updateStatus(App.StatusEnum.DISCONNECTED, 'Missing connection options'); + disconnect(); + return; } - webClient.options = {} as WebSocketConnectOptions; + // Strip credentials before handing off to session commands — they travel as + // separate function args so they can't accidentally ride along in the + // typed options object that flows downstream. + switch (options.reason) { + case App.WebSocketConnectReason.LOGIN: { + const { password, ...rest } = options; + updateStatus(App.StatusEnum.LOGGING_IN, 'Logging In...'); + if (getPasswordSalt) { + requestPasswordSalt(rest, password); + } else { + login(rest, password); + } + break; + } + case App.WebSocketConnectReason.REGISTER: { + const { password, ...rest } = options; + const passwordSalt = getPasswordSalt ? generateSalt() : null; + register(rest, password, passwordSalt); + break; + } + case App.WebSocketConnectReason.ACTIVATE_ACCOUNT: { + const { password, ...rest } = options; + if (getPasswordSalt) { + requestPasswordSalt(rest, password); + } else { + activate(rest, password); + } + break; + } + case App.WebSocketConnectReason.PASSWORD_RESET_REQUEST: + forgotPasswordRequest(options); + break; + case App.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE: + forgotPasswordChallenge(options); + break; + case App.WebSocketConnectReason.PASSWORD_RESET: { + const { newPassword, ...rest } = options; + if (getPasswordSalt) { + requestPasswordSalt(rest, undefined, newPassword); + } else { + forgotPasswordReset(rest, newPassword); + } + break; + } + default: { + const { reason } = options as Enriched.WebSocketConnectOptions; + updateStatus(App.StatusEnum.DISCONNECTED, `Unknown Connection Reason: ${reason}`); + disconnect(); + break; + } + } + + webClient.options = null; SessionPersistence.updateInfo(serverName, serverVersion); } diff --git a/webclient/src/websocket/events/session/serverMessage.ts b/webclient/src/websocket/events/session/serverMessage.ts index f9e52aa7a..08162c823 100644 --- a/webclient/src/websocket/events/session/serverMessage.ts +++ b/webclient/src/websocket/events/session/serverMessage.ts @@ -1,6 +1,6 @@ +import type { Data } from '@app/types'; import { SessionPersistence } from '../../persistence'; -import { ServerMessageData } from './interfaces'; -export function serverMessage({ message }: ServerMessageData): void { +export function serverMessage({ message }: Data.Event_ServerMessage): void { SessionPersistence.serverMessage(message); } diff --git a/webclient/src/websocket/events/session/serverShutdown.ts b/webclient/src/websocket/events/session/serverShutdown.ts index cdda893a4..33fbfad75 100644 --- a/webclient/src/websocket/events/session/serverShutdown.ts +++ b/webclient/src/websocket/events/session/serverShutdown.ts @@ -1,7 +1,6 @@ +import type { Data } from '@app/types'; import { SessionPersistence } from '../../persistence'; -import { ServerShutdownData } from './interfaces'; - -export function serverShutdown(payload: ServerShutdownData): void { +export function serverShutdown(payload: Data.Event_ServerShutdown): void { SessionPersistence.serverShutdown(payload); } diff --git a/webclient/src/websocket/events/session/sessionEvents.spec.ts b/webclient/src/websocket/events/session/sessionEvents.spec.ts index 7b35ad135..37948043d 100644 --- a/webclient/src/websocket/events/session/sessionEvents.spec.ts +++ b/webclient/src/websocket/events/session/sessionEvents.spec.ts @@ -54,22 +54,8 @@ vi.mock('../../utils', () => ({ passwordSaltSupported: vi.fn().mockReturnValue(0), })); -import { WebSocketConnectReason } from 'types'; +import { App, Data, Enriched } from '@app/types'; import { create } from '@bufbuild/protobuf'; -import { Event_ConnectionClosed_CloseReason, Event_ConnectionClosedSchema } from 'generated/proto/event_connection_closed_pb'; -import { Event_GameJoinedSchema } from 'generated/proto/event_game_joined_pb'; -import { Event_NotifyUserSchema } from 'generated/proto/event_notify_user_pb'; -import { Event_ReplayAddedSchema } from 'generated/proto/event_replay_added_pb'; -import { Event_ServerCompleteListSchema } from 'generated/proto/event_server_complete_list_pb'; -import { Event_ServerMessageSchema } from 'generated/proto/event_server_message_pb'; -import { Event_ServerShutdownSchema } from 'generated/proto/event_server_shutdown_pb'; -import { Event_UserJoinedSchema } from 'generated/proto/event_user_joined_pb'; -import { Event_UserLeftSchema } from 'generated/proto/event_user_left_pb'; -import { Event_UserMessageSchema } from 'generated/proto/event_user_message_pb'; -import { Event_AddToListSchema } from 'generated/proto/event_add_to_list_pb'; -import { Event_RemoveFromListSchema } from 'generated/proto/event_remove_from_list_pb'; -import { Event_ListRoomsSchema } from 'generated/proto/event_list_rooms_pb'; -import { Event_ServerIdentificationSchema } from 'generated/proto/event_server_identification_pb'; import { SessionPersistence, RoomPersistence } from '../../persistence'; import webClient from '../../WebClient'; @@ -92,8 +78,9 @@ import { connectionClosed } from './connectionClosed'; import { serverIdentification } from './serverIdentification'; import { Mock } from 'vitest'; +const ConfigMock = Config as { -readonly [K in keyof typeof Config]: (typeof Config)[K] }; + beforeEach(() => { - vi.clearAllMocks(); (Utils.generateSalt as Mock).mockReturnValue('newSalt'); (Utils.passwordSaltSupported as Mock).mockReturnValue(0); }); @@ -104,7 +91,7 @@ beforeEach(() => { describe('gameJoined', () => { it('calls SessionPersistence.gameJoined', () => { - const data = create(Event_GameJoinedSchema, { playerId: 1 }); + const data = create(Data.Event_GameJoinedSchema, { playerId: 1 }); gameJoined(data); expect(SessionPersistence.gameJoined).toHaveBeenCalledWith(data); }); @@ -116,7 +103,7 @@ describe('gameJoined', () => { describe('notifyUser', () => { it('calls SessionPersistence.notifyUser', () => { - const data = create(Event_NotifyUserSchema, { warningReason: 'yo' }); + const data = create(Data.Event_NotifyUserSchema, { warningReason: 'yo' }); notifyUser(data); expect(SessionPersistence.notifyUser).toHaveBeenCalledWith(data); }); @@ -128,8 +115,9 @@ describe('notifyUser', () => { describe('replayAdded', () => { it('calls SessionPersistence.replayAdded with matchInfo', () => { - const data = create(Event_ReplayAddedSchema); - data.matchInfo = { gameId: 42 } as any; + const data = create(Data.Event_ReplayAddedSchema, { + matchInfo: create(Data.ServerInfo_ReplayMatchSchema, { gameId: 42 }), + }); replayAdded(data); expect(SessionPersistence.replayAdded).toHaveBeenCalledWith(data.matchInfo); }); @@ -141,7 +129,7 @@ describe('replayAdded', () => { describe('serverCompleteList', () => { it('calls SessionPersistence.updateUsers and RoomPersistence.updateRooms', () => { - const data = create(Event_ServerCompleteListSchema, { userList: [], roomList: [] }); + const data = create(Data.Event_ServerCompleteListSchema, { userList: [], roomList: [] }); serverCompleteList(data); expect(SessionPersistence.updateUsers).toHaveBeenCalledWith(data.userList); expect(RoomPersistence.updateRooms).toHaveBeenCalledWith(data.roomList); @@ -154,7 +142,7 @@ describe('serverCompleteList', () => { describe('serverMessage', () => { it('calls SessionPersistence.serverMessage with message', () => { - serverMessage(create(Event_ServerMessageSchema, { message: 'hello server' })); + serverMessage(create(Data.Event_ServerMessageSchema, { message: 'hello server' })); expect(SessionPersistence.serverMessage).toHaveBeenCalledWith('hello server'); }); }); @@ -165,7 +153,7 @@ describe('serverMessage', () => { describe('serverShutdown', () => { it('calls SessionPersistence.serverShutdown', () => { - const payload = create(Event_ServerShutdownSchema, { reason: 'maintenance' }); + const payload = create(Data.Event_ServerShutdownSchema, { reason: 'maintenance' }); serverShutdown(payload); expect(SessionPersistence.serverShutdown).toHaveBeenCalledWith(payload); }); @@ -177,8 +165,9 @@ describe('serverShutdown', () => { describe('userJoined', () => { it('calls SessionPersistence.userJoined with userInfo', () => { - const data = create(Event_UserJoinedSchema); - data.userInfo = { name: 'alice' } as any; + const data = create(Data.Event_UserJoinedSchema, { + userInfo: create(Data.ServerInfo_UserSchema, { name: 'alice' }), + }); userJoined(data); expect(SessionPersistence.userJoined).toHaveBeenCalledWith(data.userInfo); }); @@ -190,7 +179,7 @@ describe('userJoined', () => { describe('userLeft', () => { it('calls SessionPersistence.userLeft with name', () => { - userLeft(create(Event_UserLeftSchema, { name: 'bob' })); + userLeft(create(Data.Event_UserLeftSchema, { name: 'bob' })); expect(SessionPersistence.userLeft).toHaveBeenCalledWith('bob'); }); }); @@ -201,7 +190,7 @@ describe('userLeft', () => { describe('userMessage', () => { it('calls SessionPersistence.userMessage', () => { - const payload = create(Event_UserMessageSchema, { senderName: 'alice', message: 'hi' }); + const payload = create(Data.Event_UserMessageSchema, { senderName: 'alice', message: 'hi' }); userMessage(payload); expect(SessionPersistence.userMessage).toHaveBeenCalledWith(payload); }); @@ -217,21 +206,25 @@ describe('addToList', () => { }); it('buddy list → addToBuddyList', () => { - const data = create(Event_AddToListSchema, { listName: 'buddy' }); - data.userInfo = { name: 'alice' } as any; + const data = create(Data.Event_AddToListSchema, { + listName: 'buddy', + userInfo: create(Data.ServerInfo_UserSchema, { name: 'alice' }), + }); addToList(data); expect(SessionPersistence.addToBuddyList).toHaveBeenCalledWith(data.userInfo); }); it('ignore list → addToIgnoreList', () => { - const data = create(Event_AddToListSchema, { listName: 'ignore' }); - data.userInfo = { name: 'bob' } as any; + const data = create(Data.Event_AddToListSchema, { + listName: 'ignore', + userInfo: create(Data.ServerInfo_UserSchema, { name: 'bob' }), + }); addToList(data); expect(SessionPersistence.addToIgnoreList).toHaveBeenCalledWith(data.userInfo); }); it('unknown list → console.log', () => { - addToList(create(Event_AddToListSchema, { listName: 'unknown' })); + addToList(create(Data.Event_AddToListSchema, { listName: 'unknown' })); expect(logSpy).toHaveBeenCalled(); }); }); @@ -242,18 +235,18 @@ describe('addToList', () => { describe('removeFromList', () => { it('buddy list → removeFromBuddyList', () => { - removeFromList(create(Event_RemoveFromListSchema, { listName: 'buddy', userName: 'alice' })); + removeFromList(create(Data.Event_RemoveFromListSchema, { listName: 'buddy', userName: 'alice' })); expect(SessionPersistence.removeFromBuddyList).toHaveBeenCalledWith('alice'); }); it('ignore list → removeFromIgnoreList', () => { - removeFromList(create(Event_RemoveFromListSchema, { listName: 'ignore', userName: 'bob' })); + removeFromList(create(Data.Event_RemoveFromListSchema, { listName: 'ignore', userName: 'bob' })); expect(SessionPersistence.removeFromIgnoreList).toHaveBeenCalledWith('bob'); }); it('unknown list → console.log', () => { const logSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - removeFromList(create(Event_RemoveFromListSchema, { listName: 'other', userName: 'x' })); + removeFromList(create(Data.Event_RemoveFromListSchema, { listName: 'other', userName: 'x' })); expect(logSpy).toHaveBeenCalled(); logSpy.mockRestore(); }); @@ -265,19 +258,26 @@ describe('removeFromList', () => { describe('listRooms', () => { it('calls RoomPersistence.updateRooms', () => { - listRooms(create(Event_ListRoomsSchema, { roomList: [] })); + listRooms(create(Data.Event_ListRoomsSchema, { roomList: [] })); expect(RoomPersistence.updateRooms).toHaveBeenCalledWith([]); }); it('does not call joinRoom when autojoinrooms is false', () => { - (Config as any).CLIENT_OPTIONS = { autojoinrooms: false }; - listRooms(create(Event_ListRoomsSchema, { roomList: [{ autoJoin: true, roomId: 1 }] as any[] })); + ConfigMock.CLIENT_OPTIONS = { autojoinrooms: false }; + listRooms(create(Data.Event_ListRoomsSchema, { + roomList: [create(Data.ServerInfo_RoomSchema, { autoJoin: true, roomId: 1 })] + })); expect(SessionCmds.joinRoom).not.toHaveBeenCalled(); }); it('calls joinRoom for autoJoin rooms when autojoinrooms is true', () => { - (Config as any).CLIENT_OPTIONS = { autojoinrooms: true }; - listRooms(create(Event_ListRoomsSchema, { roomList: [{ autoJoin: true, roomId: 2 }, { autoJoin: false, roomId: 3 }] as any[] })); + ConfigMock.CLIENT_OPTIONS = { autojoinrooms: true }; + listRooms(create(Data.Event_ListRoomsSchema, { + roomList: [ + create(Data.ServerInfo_RoomSchema, { autoJoin: true, roomId: 2 }), + create(Data.ServerInfo_RoomSchema, { autoJoin: false, roomId: 3 }) + ] + })); expect(SessionCmds.joinRoom).toHaveBeenCalledTimes(1); expect(SessionCmds.joinRoom).toHaveBeenCalledWith(2); }); @@ -289,12 +289,12 @@ describe('listRooms', () => { describe('connectionClosed', () => { it('uses reasonStr when provided', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: 0, reasonStr: 'custom' })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: 0, reasonStr: 'custom' })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'custom'); }); it('USER_LIMIT_REACHED → specific message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.USER_LIMIT_REACHED })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.USER_LIMIT_REACHED })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith( expect.anything(), expect.stringContaining('maximum user capacity') @@ -302,42 +302,44 @@ describe('connectionClosed', () => { }); it('TOO_MANY_CONNECTIONS → specific message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.TOO_MANY_CONNECTIONS })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.TOO_MANY_CONNECTIONS })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('too many concurrent')); }); it('BANNED → specific message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.BANNED })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.BANNED })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'You are banned'); }); it('DEMOTED → specific message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.DEMOTED })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.DEMOTED })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('demoted')); }); it('SERVER_SHUTDOWN → specific message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.SERVER_SHUTDOWN })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.SERVER_SHUTDOWN })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('shutdown')); }); it('USERNAMEINVALID → specific message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.USERNAMEINVALID })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.USERNAMEINVALID })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('username')); }); it('LOGGEDINELSEWERE → specific message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.LOGGEDINELSEWERE })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.LOGGEDINELSEWERE })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), expect.stringContaining('logged out')); }); it('OTHER → "Unknown reason"', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.OTHER })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.OTHER })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'Unknown reason'); }); it('BANNED with valid positive endTime → shows formatted date', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.BANNED, endTime: 1700000000 })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { + reason: Data.Event_ConnectionClosed_CloseReason.BANNED, endTime: 1700000000, + })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith( expect.anything(), expect.stringContaining('You are banned until') @@ -345,28 +347,30 @@ describe('connectionClosed', () => { }); it('BANNED with endTime = 0 → shows generic banned message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.BANNED, endTime: 0 })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.BANNED, endTime: 0 })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'You are banned'); }); it('BANNED with endTime = -1 → shows generic banned message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.BANNED, endTime: -1 })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.BANNED, endTime: -1 })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'You are banned'); }); it('BANNED with endTime = NaN → shows generic banned message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.BANNED, endTime: NaN })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { reason: Data.Event_ConnectionClosed_CloseReason.BANNED, endTime: NaN })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'You are banned'); }); it('BANNED with endTime = Infinity → shows generic banned message', () => { - connectionClosed(create(Event_ConnectionClosedSchema, { reason: Event_ConnectionClosed_CloseReason.BANNED, endTime: Infinity })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, { + reason: Data.Event_ConnectionClosed_CloseReason.BANNED, endTime: Infinity, + })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'You are banned'); }); it('BANNED with reasonStr → uses reasonStr regardless of endTime', () => { - connectionClosed(create(Event_ConnectionClosedSchema, - { reason: Event_ConnectionClosed_CloseReason.BANNED, endTime: 0, reasonStr: 'custom ban reason' })); + connectionClosed(create(Data.Event_ConnectionClosedSchema, + { reason: Data.Event_ConnectionClosed_CloseReason.BANNED, endTime: 0, reasonStr: 'custom ban reason' })); expect(SessionCmds.updateStatus).toHaveBeenCalledWith(expect.anything(), 'custom ban reason'); }); }); @@ -377,21 +381,21 @@ describe('connectionClosed', () => { describe('serverIdentification', () => { beforeEach(() => { - (Config as any).PROTOCOL_VERSION = 14; - (webClient as any).options = {}; + ConfigMock.PROTOCOL_VERSION = 14; + webClient.options = null; }); it('disconnects when protocolVersion mismatches', () => { - serverIdentification(create(Event_ServerIdentificationSchema, + serverIdentification(create(Data.Event_ServerIdentificationSchema, { serverName: 's', serverVersion: '1', protocolVersion: 99, serverOptions: 0 })); expect(SessionCmds.updateStatus).toHaveBeenCalled(); expect(SessionCmds.disconnect).toHaveBeenCalled(); }); it('LOGIN reason without salt → calls login with password as separate param', () => { - (webClient as any).options = { reason: WebSocketConnectReason.LOGIN, password: 'secret' }; + webClient.options = { host: 'h', port: '1', userName: 'u', reason: App.WebSocketConnectReason.LOGIN, password: 'secret' }; (Utils.passwordSaltSupported as Mock).mockReturnValue(0); - serverIdentification(create(Event_ServerIdentificationSchema, + serverIdentification(create(Data.Event_ServerIdentificationSchema, { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 })); expect(SessionCmds.login).toHaveBeenCalledWith( expect.not.objectContaining({ password: expect.anything() }), @@ -400,9 +404,9 @@ describe('serverIdentification', () => { }); it('LOGIN reason with salt → calls requestPasswordSalt with password as separate param', () => { - (webClient as any).options = { reason: WebSocketConnectReason.LOGIN, password: 'secret' }; + webClient.options = { host: 'h', port: '1', userName: 'u', reason: App.WebSocketConnectReason.LOGIN, password: 'secret' }; (Utils.passwordSaltSupported as Mock).mockReturnValue(1); - serverIdentification(create(Event_ServerIdentificationSchema, + serverIdentification(create(Data.Event_ServerIdentificationSchema, { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 })); expect(SessionCmds.requestPasswordSalt).toHaveBeenCalledWith( expect.not.objectContaining({ password: expect.anything() }), @@ -411,9 +415,12 @@ describe('serverIdentification', () => { }); it('REGISTER reason without salt → calls register with password and null salt', () => { - (webClient as any).options = { reason: WebSocketConnectReason.REGISTER, password: 'secret' }; + webClient.options = { + host: 'h', port: '1', userName: 'u', email: 'e', country: 'US', realName: 'R', + reason: App.WebSocketConnectReason.REGISTER, password: 'secret', + }; (Utils.passwordSaltSupported as Mock).mockReturnValue(0); - serverIdentification(create(Event_ServerIdentificationSchema, + serverIdentification(create(Data.Event_ServerIdentificationSchema, { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 })); expect(SessionCmds.register).toHaveBeenCalledWith( expect.not.objectContaining({ password: expect.anything() }), @@ -423,9 +430,12 @@ describe('serverIdentification', () => { }); it('REGISTER reason with salt → calls register with password and generated salt', () => { - (webClient as any).options = { reason: WebSocketConnectReason.REGISTER, password: 'secret' }; + webClient.options = { + host: 'h', port: '1', userName: 'u', email: 'e', country: 'US', realName: 'R', + reason: App.WebSocketConnectReason.REGISTER, password: 'secret', + }; (Utils.passwordSaltSupported as Mock).mockReturnValue(1); - serverIdentification(create(Event_ServerIdentificationSchema, + serverIdentification(create(Data.Event_ServerIdentificationSchema, { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 })); expect(SessionCmds.register).toHaveBeenCalledWith( expect.not.objectContaining({ password: expect.anything() }), @@ -435,9 +445,12 @@ describe('serverIdentification', () => { }); it('ACTIVATE_ACCOUNT reason without salt → calls activate with password as separate param', () => { - (webClient as any).options = { reason: WebSocketConnectReason.ACTIVATE_ACCOUNT, password: 'secret' }; + webClient.options = { + host: 'h', port: '1', userName: 'u', token: 'tok', + reason: App.WebSocketConnectReason.ACTIVATE_ACCOUNT, password: 'secret', + }; (Utils.passwordSaltSupported as Mock).mockReturnValue(0); - serverIdentification(create(Event_ServerIdentificationSchema, + serverIdentification(create(Data.Event_ServerIdentificationSchema, { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 })); expect(SessionCmds.activate).toHaveBeenCalledWith( expect.not.objectContaining({ password: expect.anything() }), @@ -446,9 +459,12 @@ describe('serverIdentification', () => { }); it('ACTIVATE_ACCOUNT reason with salt → calls requestPasswordSalt with password as separate param', () => { - (webClient as any).options = { reason: WebSocketConnectReason.ACTIVATE_ACCOUNT, password: 'secret' }; + webClient.options = { + host: 'h', port: '1', userName: 'u', token: 'tok', + reason: App.WebSocketConnectReason.ACTIVATE_ACCOUNT, password: 'secret', + }; (Utils.passwordSaltSupported as Mock).mockReturnValue(1); - serverIdentification(create(Event_ServerIdentificationSchema, + serverIdentification(create(Data.Event_ServerIdentificationSchema, { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 })); expect(SessionCmds.requestPasswordSalt).toHaveBeenCalledWith( expect.not.objectContaining({ password: expect.anything() }), @@ -457,23 +473,26 @@ describe('serverIdentification', () => { }); it('PASSWORD_RESET_REQUEST reason → calls forgotPasswordRequest', () => { - (webClient as any).options = { reason: WebSocketConnectReason.PASSWORD_RESET_REQUEST }; - serverIdentification(create(Event_ServerIdentificationSchema, + webClient.options = { host: 'h', port: '1', userName: 'u', reason: App.WebSocketConnectReason.PASSWORD_RESET_REQUEST }; + serverIdentification(create(Data.Event_ServerIdentificationSchema, { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 })); expect(SessionCmds.forgotPasswordRequest).toHaveBeenCalled(); }); it('PASSWORD_RESET_CHALLENGE reason → calls forgotPasswordChallenge', () => { - (webClient as any).options = { reason: WebSocketConnectReason.PASSWORD_RESET_CHALLENGE }; - serverIdentification(create(Event_ServerIdentificationSchema, + webClient.options = { host: 'h', port: '1', userName: 'u', email: 'e', reason: App.WebSocketConnectReason.PASSWORD_RESET_CHALLENGE }; + serverIdentification(create(Data.Event_ServerIdentificationSchema, { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 })); expect(SessionCmds.forgotPasswordChallenge).toHaveBeenCalled(); }); it('PASSWORD_RESET reason without salt → calls forgotPasswordReset with newPassword as separate param', () => { - (webClient as any).options = { reason: WebSocketConnectReason.PASSWORD_RESET, newPassword: 'newpw' }; + webClient.options = { + host: 'h', port: '1', userName: 'u', token: 'tok', + reason: App.WebSocketConnectReason.PASSWORD_RESET, newPassword: 'newpw', + }; (Utils.passwordSaltSupported as Mock).mockReturnValue(0); - serverIdentification(create(Event_ServerIdentificationSchema, + serverIdentification(create(Data.Event_ServerIdentificationSchema, { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 })); expect(SessionCmds.forgotPasswordReset).toHaveBeenCalledWith( expect.not.objectContaining({ newPassword: expect.anything() }), @@ -482,9 +501,12 @@ describe('serverIdentification', () => { }); it('PASSWORD_RESET reason with salt → calls requestPasswordSalt with newPassword as separate param', () => { - (webClient as any).options = { reason: WebSocketConnectReason.PASSWORD_RESET, newPassword: 'newpw' }; + webClient.options = { + host: 'h', port: '1', userName: 'u', token: 'tok', + reason: App.WebSocketConnectReason.PASSWORD_RESET, newPassword: 'newpw', + }; (Utils.passwordSaltSupported as Mock).mockReturnValue(1); - serverIdentification(create(Event_ServerIdentificationSchema, + serverIdentification(create(Data.Event_ServerIdentificationSchema, { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 1 })); expect(SessionCmds.requestPasswordSalt).toHaveBeenCalledWith( expect.not.objectContaining({ newPassword: expect.anything() }), @@ -494,18 +516,18 @@ describe('serverIdentification', () => { }); it('unknown reason → updateStatus DISCONNECTED and disconnect', () => { - (webClient as any).options = { reason: 999 }; - serverIdentification(create(Event_ServerIdentificationSchema, + webClient.options = { host: 'h', port: '1', reason: 999 as App.WebSocketConnectReason } as Enriched.WebSocketConnectOptions; + serverIdentification(create(Data.Event_ServerIdentificationSchema, { serverName: 's', serverVersion: '1', protocolVersion: 14, serverOptions: 0 })); expect(SessionCmds.updateStatus).toHaveBeenCalled(); expect(SessionCmds.disconnect).toHaveBeenCalled(); }); - it('updates webClient.options to empty and calls SessionPersistence.updateInfo', () => { - (webClient as any).options = { reason: WebSocketConnectReason.LOGIN }; - serverIdentification(create(Event_ServerIdentificationSchema, + it('resets webClient.options and calls SessionPersistence.updateInfo', () => { + webClient.options = { host: 'h', port: '1', userName: 'u', reason: App.WebSocketConnectReason.LOGIN }; + serverIdentification(create(Data.Event_ServerIdentificationSchema, { serverName: 'myServer', serverVersion: '2.0', protocolVersion: 14, serverOptions: 0 })); expect(SessionPersistence.updateInfo).toHaveBeenCalledWith('myServer', '2.0'); - expect((webClient as any).options).toEqual({}); + expect(webClient.options).toBeNull(); }); }); diff --git a/webclient/src/websocket/events/session/userJoined.ts b/webclient/src/websocket/events/session/userJoined.ts index cb512db60..99fde6008 100644 --- a/webclient/src/websocket/events/session/userJoined.ts +++ b/webclient/src/websocket/events/session/userJoined.ts @@ -1,6 +1,6 @@ +import type { Data } from '@app/types'; import { SessionPersistence } from '../../persistence'; -import { UserJoinedData } from './interfaces'; -export function userJoined({ userInfo }: UserJoinedData): void { +export function userJoined({ userInfo }: Data.Event_UserJoined): void { SessionPersistence.userJoined(userInfo); } diff --git a/webclient/src/websocket/events/session/userLeft.ts b/webclient/src/websocket/events/session/userLeft.ts index 9e00e59e1..83d8404ed 100644 --- a/webclient/src/websocket/events/session/userLeft.ts +++ b/webclient/src/websocket/events/session/userLeft.ts @@ -1,6 +1,6 @@ +import type { Data } from '@app/types'; import { SessionPersistence } from '../../persistence'; -import { UserLeftData } from './interfaces'; -export function userLeft({ name }: UserLeftData): void { +export function userLeft({ name }: Data.Event_UserLeft): void { SessionPersistence.userLeft(name); } diff --git a/webclient/src/websocket/events/session/userMessage.ts b/webclient/src/websocket/events/session/userMessage.ts index bff08460b..073c22de6 100644 --- a/webclient/src/websocket/events/session/userMessage.ts +++ b/webclient/src/websocket/events/session/userMessage.ts @@ -1,8 +1,6 @@ +import type { Data } from '@app/types'; import { SessionPersistence } from '../../persistence'; -import { UserMessageData } from './interfaces'; - - -export function userMessage(payload: UserMessageData): void { +export function userMessage(payload: Data.Event_UserMessage): void { SessionPersistence.userMessage(payload); } diff --git a/webclient/src/websocket/persistence/AdminPersistence.spec.ts b/webclient/src/websocket/persistence/AdminPersistence.spec.ts index 6eb54f7e1..6f08dea8b 100644 --- a/webclient/src/websocket/persistence/AdminPersistence.spec.ts +++ b/webclient/src/websocket/persistence/AdminPersistence.spec.ts @@ -1,4 +1,4 @@ -vi.mock('store', () => ({ +vi.mock('@app/store', () => ({ ServerDispatch: { adjustMod: vi.fn(), reloadConfig: vi.fn(), @@ -8,11 +8,7 @@ vi.mock('store', () => ({ })); import { AdminPersistence } from './AdminPersistence'; -import { ServerDispatch } from 'store'; - -beforeEach(() => { - vi.clearAllMocks(); -}); +import { ServerDispatch } from '@app/store'; describe('AdminPersistence', () => { it('adjustMod passes userName, shouldBeMod, shouldBeJudge', () => { diff --git a/webclient/src/websocket/persistence/AdminPersistence.ts b/webclient/src/websocket/persistence/AdminPersistence.ts index 9552d8abf..0f93fef9d 100644 --- a/webclient/src/websocket/persistence/AdminPersistence.ts +++ b/webclient/src/websocket/persistence/AdminPersistence.ts @@ -1,4 +1,4 @@ -import { ServerDispatch } from 'store'; +import { ServerDispatch } from '@app/store'; export class AdminPersistence { static adjustMod(userName: string, shouldBeMod: boolean, shouldBeJudge: boolean) { diff --git a/webclient/src/websocket/persistence/GamePersistence.spec.ts b/webclient/src/websocket/persistence/GamePersistence.spec.ts index 50d8c6539..996faca09 100644 --- a/webclient/src/websocket/persistence/GamePersistence.spec.ts +++ b/webclient/src/websocket/persistence/GamePersistence.spec.ts @@ -1,7 +1,7 @@ import { create } from '@bufbuild/protobuf'; import { GamePersistence } from './GamePersistence'; -vi.mock('store', () => ({ +vi.mock('@app/store', () => ({ GameDispatch: { gameStateChanged: vi.fn(), playerJoined: vi.fn(), @@ -35,40 +35,19 @@ vi.mock('store', () => ({ }, })); -import { Event_GameStateChangedSchema } from 'generated/proto/event_game_state_changed_pb'; -import { Event_MoveCardSchema } from 'generated/proto/event_move_card_pb'; -import { Event_FlipCardSchema } from 'generated/proto/event_flip_card_pb'; -import { Event_DestroyCardSchema } from 'generated/proto/event_destroy_card_pb'; -import { Event_AttachCardSchema } from 'generated/proto/event_attach_card_pb'; -import { Event_CreateTokenSchema } from 'generated/proto/event_create_token_pb'; -import { Event_SetCardAttrSchema } from 'generated/proto/event_set_card_attr_pb'; -import { Event_SetCardCounterSchema } from 'generated/proto/event_set_card_counter_pb'; -import { Event_CreateArrowSchema } from 'generated/proto/event_create_arrow_pb'; -import { Event_DeleteArrowSchema } from 'generated/proto/event_delete_arrow_pb'; -import { Event_CreateCounterSchema } from 'generated/proto/event_create_counter_pb'; -import { Event_SetCounterSchema } from 'generated/proto/event_set_counter_pb'; -import { Event_DelCounterSchema } from 'generated/proto/event_del_counter_pb'; -import { Event_DrawCardsSchema } from 'generated/proto/event_draw_cards_pb'; -import { Event_RevealCardsSchema } from 'generated/proto/event_reveal_cards_pb'; -import { Event_ShuffleSchema } from 'generated/proto/event_shuffle_pb'; -import { Event_RollDieSchema } from 'generated/proto/event_roll_die_pb'; -import { Event_DumpZoneSchema } from 'generated/proto/event_dump_zone_pb'; -import { Event_ChangeZonePropertiesSchema } from 'generated/proto/event_change_zone_properties_pb'; -import { ServerInfo_PlayerPropertiesSchema } from 'generated/proto/serverinfo_playerproperties_pb'; +import { Data } from '@app/types'; -import { GameDispatch } from 'store'; - -beforeEach(() => vi.clearAllMocks()); +import { GameDispatch } from '@app/store'; describe('GamePersistence', () => { it('gameStateChanged dispatches via GameDispatch', () => { - const data = create(Event_GameStateChangedSchema, { playerList: [] }); + const data = create(Data.Event_GameStateChangedSchema, { playerList: [] }); GamePersistence.gameStateChanged(5, data); expect(GameDispatch.gameStateChanged).toHaveBeenCalledWith(5, data); }); it('playerJoined dispatches via GameDispatch', () => { - const data = create(ServerInfo_PlayerPropertiesSchema, { playerId: 1 }); + const data = create(Data.ServerInfo_PlayerPropertiesSchema, { playerId: 1 }); GamePersistence.playerJoined(5, data); expect(GameDispatch.playerJoined).toHaveBeenCalledWith(5, data); }); @@ -79,7 +58,7 @@ describe('GamePersistence', () => { }); it('playerPropertiesChanged dispatches via GameDispatch', () => { - const props = create(ServerInfo_PlayerPropertiesSchema, { playerId: 2 }); + const props = create(Data.ServerInfo_PlayerPropertiesSchema, { playerId: 2 }); GamePersistence.playerPropertiesChanged(5, 2, props); expect(GameDispatch.playerPropertiesChanged).toHaveBeenCalledWith(5, 2, props); }); @@ -105,97 +84,97 @@ describe('GamePersistence', () => { }); it('cardMoved dispatches via GameDispatch', () => { - const data = create(Event_MoveCardSchema, { cardId: 3 }); + const data = create(Data.Event_MoveCardSchema, { cardId: 3 }); GamePersistence.cardMoved(5, 1, data); expect(GameDispatch.cardMoved).toHaveBeenCalledWith(5, 1, data); }); it('cardFlipped dispatches via GameDispatch', () => { - const data = create(Event_FlipCardSchema, { cardId: 3 }); + const data = create(Data.Event_FlipCardSchema, { cardId: 3 }); GamePersistence.cardFlipped(5, 1, data); expect(GameDispatch.cardFlipped).toHaveBeenCalledWith(5, 1, data); }); it('cardDestroyed dispatches via GameDispatch', () => { - const data = create(Event_DestroyCardSchema, { cardId: 3 }); + const data = create(Data.Event_DestroyCardSchema, { cardId: 3 }); GamePersistence.cardDestroyed(5, 1, data); expect(GameDispatch.cardDestroyed).toHaveBeenCalledWith(5, 1, data); }); it('cardAttached dispatches via GameDispatch', () => { - const data = create(Event_AttachCardSchema, { cardId: 3 }); + const data = create(Data.Event_AttachCardSchema, { cardId: 3 }); GamePersistence.cardAttached(5, 1, data); expect(GameDispatch.cardAttached).toHaveBeenCalledWith(5, 1, data); }); it('tokenCreated dispatches via GameDispatch', () => { - const data = create(Event_CreateTokenSchema, { cardId: 3 }); + const data = create(Data.Event_CreateTokenSchema, { cardId: 3 }); GamePersistence.tokenCreated(5, 1, data); expect(GameDispatch.tokenCreated).toHaveBeenCalledWith(5, 1, data); }); it('cardAttrChanged dispatches via GameDispatch', () => { - const data = create(Event_SetCardAttrSchema, { cardId: 3 }); + const data = create(Data.Event_SetCardAttrSchema, { cardId: 3 }); GamePersistence.cardAttrChanged(5, 1, data); expect(GameDispatch.cardAttrChanged).toHaveBeenCalledWith(5, 1, data); }); it('cardCounterChanged dispatches via GameDispatch', () => { - const data = create(Event_SetCardCounterSchema, { cardId: 3 }); + const data = create(Data.Event_SetCardCounterSchema, { cardId: 3 }); GamePersistence.cardCounterChanged(5, 1, data); expect(GameDispatch.cardCounterChanged).toHaveBeenCalledWith(5, 1, data); }); it('arrowCreated dispatches via GameDispatch', () => { - const data = create(Event_CreateArrowSchema, {}); + const data = create(Data.Event_CreateArrowSchema, {}); GamePersistence.arrowCreated(5, 1, data); expect(GameDispatch.arrowCreated).toHaveBeenCalledWith(5, 1, data); }); it('arrowDeleted dispatches via GameDispatch', () => { - const data = create(Event_DeleteArrowSchema, { arrowId: 9 }); + const data = create(Data.Event_DeleteArrowSchema, { arrowId: 9 }); GamePersistence.arrowDeleted(5, 1, data); expect(GameDispatch.arrowDeleted).toHaveBeenCalledWith(5, 1, data); }); it('counterCreated dispatches via GameDispatch', () => { - const data = create(Event_CreateCounterSchema, {}); + const data = create(Data.Event_CreateCounterSchema, {}); GamePersistence.counterCreated(5, 1, data); expect(GameDispatch.counterCreated).toHaveBeenCalledWith(5, 1, data); }); it('counterSet dispatches via GameDispatch', () => { - const data = create(Event_SetCounterSchema, { counterId: 1, value: 20 }); + const data = create(Data.Event_SetCounterSchema, { counterId: 1, value: 20 }); GamePersistence.counterSet(5, 1, data); expect(GameDispatch.counterSet).toHaveBeenCalledWith(5, 1, data); }); it('counterDeleted dispatches via GameDispatch', () => { - const data = create(Event_DelCounterSchema, { counterId: 1 }); + const data = create(Data.Event_DelCounterSchema, { counterId: 1 }); GamePersistence.counterDeleted(5, 1, data); expect(GameDispatch.counterDeleted).toHaveBeenCalledWith(5, 1, data); }); it('cardsDrawn dispatches via GameDispatch', () => { - const data = create(Event_DrawCardsSchema, { number: 2, cards: [] }); + const data = create(Data.Event_DrawCardsSchema, { number: 2, cards: [] }); GamePersistence.cardsDrawn(5, 1, data); expect(GameDispatch.cardsDrawn).toHaveBeenCalledWith(5, 1, data); }); it('cardsRevealed dispatches via GameDispatch', () => { - const data = create(Event_RevealCardsSchema, { zoneName: 'hand', cards: [] }); + const data = create(Data.Event_RevealCardsSchema, { zoneName: 'hand', cards: [] }); GamePersistence.cardsRevealed(5, 1, data); expect(GameDispatch.cardsRevealed).toHaveBeenCalledWith(5, 1, data); }); it('zoneShuffled dispatches via GameDispatch', () => { - const data = create(Event_ShuffleSchema, { zoneName: 'deck' }); + const data = create(Data.Event_ShuffleSchema, { zoneName: 'deck' }); GamePersistence.zoneShuffled(5, 1, data); expect(GameDispatch.zoneShuffled).toHaveBeenCalledWith(5, 1, data); }); it('dieRolled dispatches via GameDispatch', () => { - const data = create(Event_RollDieSchema, { sides: 6, value: 4 }); + const data = create(Data.Event_RollDieSchema, { sides: 6, value: 4 }); GamePersistence.dieRolled(5, 1, data); expect(GameDispatch.dieRolled).toHaveBeenCalledWith(5, 1, data); }); @@ -216,13 +195,13 @@ describe('GamePersistence', () => { }); it('zoneDumped dispatches via GameDispatch', () => { - const data = create(Event_DumpZoneSchema, { zoneName: 'hand' }); + const data = create(Data.Event_DumpZoneSchema, { zoneName: 'hand' }); GamePersistence.zoneDumped(5, 1, data); expect(GameDispatch.zoneDumped).toHaveBeenCalledWith(5, 1, data); }); it('zonePropertiesChanged dispatches via GameDispatch', () => { - const data = create(Event_ChangeZonePropertiesSchema, { zoneName: 'hand', alwaysRevealTopCard: true }); + const data = create(Data.Event_ChangeZonePropertiesSchema, { zoneName: 'hand', alwaysRevealTopCard: true }); GamePersistence.zonePropertiesChanged(5, 1, data); expect(GameDispatch.zonePropertiesChanged).toHaveBeenCalledWith(5, 1, data); }); diff --git a/webclient/src/websocket/persistence/GamePersistence.ts b/webclient/src/websocket/persistence/GamePersistence.ts index e21a9db45..fd31aa44a 100644 --- a/webclient/src/websocket/persistence/GamePersistence.ts +++ b/webclient/src/websocket/persistence/GamePersistence.ts @@ -1,31 +1,12 @@ -import { GameDispatch } from 'store'; -import type { Event_AttachCard } from 'generated/proto/event_attach_card_pb'; -import type { Event_ChangeZoneProperties } from 'generated/proto/event_change_zone_properties_pb'; -import type { Event_CreateArrow } from 'generated/proto/event_create_arrow_pb'; -import type { Event_CreateCounter } from 'generated/proto/event_create_counter_pb'; -import type { Event_CreateToken } from 'generated/proto/event_create_token_pb'; -import type { Event_DelCounter } from 'generated/proto/event_del_counter_pb'; -import type { Event_DeleteArrow } from 'generated/proto/event_delete_arrow_pb'; -import type { Event_DestroyCard } from 'generated/proto/event_destroy_card_pb'; -import type { Event_DrawCards } from 'generated/proto/event_draw_cards_pb'; -import type { Event_DumpZone } from 'generated/proto/event_dump_zone_pb'; -import type { Event_FlipCard } from 'generated/proto/event_flip_card_pb'; -import type { Event_GameStateChanged } from 'generated/proto/event_game_state_changed_pb'; -import type { Event_MoveCard } from 'generated/proto/event_move_card_pb'; -import type { ServerInfo_PlayerProperties } from 'generated/proto/serverinfo_playerproperties_pb'; -import type { Event_RevealCards } from 'generated/proto/event_reveal_cards_pb'; -import type { Event_RollDie } from 'generated/proto/event_roll_die_pb'; -import type { Event_SetCardAttr } from 'generated/proto/event_set_card_attr_pb'; -import type { Event_SetCardCounter } from 'generated/proto/event_set_card_counter_pb'; -import type { Event_SetCounter } from 'generated/proto/event_set_counter_pb'; -import type { Event_Shuffle } from 'generated/proto/event_shuffle_pb'; +import { GameDispatch } from '@app/store'; +import { Data } from '@app/types'; export class GamePersistence { - static gameStateChanged(gameId: number, data: Event_GameStateChanged): void { + static gameStateChanged(gameId: number, data: Data.Event_GameStateChanged): void { GameDispatch.gameStateChanged(gameId, data); } - static playerJoined(gameId: number, playerProperties: ServerInfo_PlayerProperties): void { + static playerJoined(gameId: number, playerProperties: Data.ServerInfo_PlayerProperties): void { GameDispatch.playerJoined(gameId, playerProperties); } @@ -33,7 +14,7 @@ export class GamePersistence { GameDispatch.playerLeft(gameId, playerId, reason); } - static playerPropertiesChanged(gameId: number, playerId: number, properties: ServerInfo_PlayerProperties): void { + static playerPropertiesChanged(gameId: number, playerId: number, properties: Data.ServerInfo_PlayerProperties): void { GameDispatch.playerPropertiesChanged(gameId, playerId, properties); } @@ -53,67 +34,67 @@ export class GamePersistence { GameDispatch.gameSay(gameId, playerId, message); } - static cardMoved(gameId: number, playerId: number, data: Event_MoveCard): void { + static cardMoved(gameId: number, playerId: number, data: Data.Event_MoveCard): void { GameDispatch.cardMoved(gameId, playerId, data); } - static cardFlipped(gameId: number, playerId: number, data: Event_FlipCard): void { + static cardFlipped(gameId: number, playerId: number, data: Data.Event_FlipCard): void { GameDispatch.cardFlipped(gameId, playerId, data); } - static cardDestroyed(gameId: number, playerId: number, data: Event_DestroyCard): void { + static cardDestroyed(gameId: number, playerId: number, data: Data.Event_DestroyCard): void { GameDispatch.cardDestroyed(gameId, playerId, data); } - static cardAttached(gameId: number, playerId: number, data: Event_AttachCard): void { + static cardAttached(gameId: number, playerId: number, data: Data.Event_AttachCard): void { GameDispatch.cardAttached(gameId, playerId, data); } - static tokenCreated(gameId: number, playerId: number, data: Event_CreateToken): void { + static tokenCreated(gameId: number, playerId: number, data: Data.Event_CreateToken): void { GameDispatch.tokenCreated(gameId, playerId, data); } - static cardAttrChanged(gameId: number, playerId: number, data: Event_SetCardAttr): void { + static cardAttrChanged(gameId: number, playerId: number, data: Data.Event_SetCardAttr): void { GameDispatch.cardAttrChanged(gameId, playerId, data); } - static cardCounterChanged(gameId: number, playerId: number, data: Event_SetCardCounter): void { + static cardCounterChanged(gameId: number, playerId: number, data: Data.Event_SetCardCounter): void { GameDispatch.cardCounterChanged(gameId, playerId, data); } - static arrowCreated(gameId: number, playerId: number, data: Event_CreateArrow): void { + static arrowCreated(gameId: number, playerId: number, data: Data.Event_CreateArrow): void { GameDispatch.arrowCreated(gameId, playerId, data); } - static arrowDeleted(gameId: number, playerId: number, data: Event_DeleteArrow): void { + static arrowDeleted(gameId: number, playerId: number, data: Data.Event_DeleteArrow): void { GameDispatch.arrowDeleted(gameId, playerId, data); } - static counterCreated(gameId: number, playerId: number, data: Event_CreateCounter): void { + static counterCreated(gameId: number, playerId: number, data: Data.Event_CreateCounter): void { GameDispatch.counterCreated(gameId, playerId, data); } - static counterSet(gameId: number, playerId: number, data: Event_SetCounter): void { + static counterSet(gameId: number, playerId: number, data: Data.Event_SetCounter): void { GameDispatch.counterSet(gameId, playerId, data); } - static counterDeleted(gameId: number, playerId: number, data: Event_DelCounter): void { + static counterDeleted(gameId: number, playerId: number, data: Data.Event_DelCounter): void { GameDispatch.counterDeleted(gameId, playerId, data); } - static cardsDrawn(gameId: number, playerId: number, data: Event_DrawCards): void { + static cardsDrawn(gameId: number, playerId: number, data: Data.Event_DrawCards): void { GameDispatch.cardsDrawn(gameId, playerId, data); } - static cardsRevealed(gameId: number, playerId: number, data: Event_RevealCards): void { + static cardsRevealed(gameId: number, playerId: number, data: Data.Event_RevealCards): void { GameDispatch.cardsRevealed(gameId, playerId, data); } - static zoneShuffled(gameId: number, playerId: number, data: Event_Shuffle): void { + static zoneShuffled(gameId: number, playerId: number, data: Data.Event_Shuffle): void { GameDispatch.zoneShuffled(gameId, playerId, data); } - static dieRolled(gameId: number, playerId: number, data: Event_RollDie): void { + static dieRolled(gameId: number, playerId: number, data: Data.Event_RollDie): void { GameDispatch.dieRolled(gameId, playerId, data); } @@ -129,11 +110,11 @@ export class GamePersistence { GameDispatch.turnReversed(gameId, reversed); } - static zoneDumped(gameId: number, playerId: number, data: Event_DumpZone): void { + static zoneDumped(gameId: number, playerId: number, data: Data.Event_DumpZone): void { GameDispatch.zoneDumped(gameId, playerId, data); } - static zonePropertiesChanged(gameId: number, playerId: number, data: Event_ChangeZoneProperties): void { + static zonePropertiesChanged(gameId: number, playerId: number, data: Data.Event_ChangeZoneProperties): void { GameDispatch.zonePropertiesChanged(gameId, playerId, data); } } diff --git a/webclient/src/websocket/persistence/ModeratorPersistence.spec.ts b/webclient/src/websocket/persistence/ModeratorPersistence.spec.ts index bd2971e10..4519a5a31 100644 --- a/webclient/src/websocket/persistence/ModeratorPersistence.spec.ts +++ b/webclient/src/websocket/persistence/ModeratorPersistence.spec.ts @@ -1,4 +1,4 @@ -vi.mock('store', () => ({ +vi.mock('@app/store', () => ({ ServerDispatch: { banFromServer: vi.fn(), banHistory: vi.fn(), @@ -14,11 +14,9 @@ vi.mock('store', () => ({ })); import { ModeratorPersistence } from './ModeratorPersistence'; -import { ServerDispatch } from 'store'; - -beforeEach(() => { - vi.clearAllMocks(); -}); +import { ServerDispatch } from '@app/store'; +import { create } from '@bufbuild/protobuf'; +import { Data } from '@app/types'; describe('ModeratorPersistence', () => { it('banFromServer passes userName', () => { @@ -32,7 +30,7 @@ describe('ModeratorPersistence', () => { }); it('viewLogs dispatches raw logs', () => { - const logs = [{ targetType: 'room' }] as any; + const logs = [create(Data.ServerInfo_ChatMessageSchema, { targetType: 'room' })]; ModeratorPersistence.viewLogs(logs); expect(ServerDispatch.viewLogs).toHaveBeenCalledWith(logs); }); diff --git a/webclient/src/websocket/persistence/ModeratorPersistence.ts b/webclient/src/websocket/persistence/ModeratorPersistence.ts index bb50fe726..2a640a6f3 100644 --- a/webclient/src/websocket/persistence/ModeratorPersistence.ts +++ b/webclient/src/websocket/persistence/ModeratorPersistence.ts @@ -1,27 +1,24 @@ -import { ServerDispatch } from 'store'; -import type { ServerInfo_Ban } from 'generated/proto/serverinfo_ban_pb'; -import type { ServerInfo_ChatMessage } from 'generated/proto/serverinfo_chat_message_pb'; -import type { ServerInfo_Warning } from 'generated/proto/serverinfo_warning_pb'; -import type { Response_WarnList } from 'generated/proto/response_warn_list_pb'; +import { ServerDispatch } from '@app/store'; +import { Data } from '@app/types'; export class ModeratorPersistence { static banFromServer(userName: string): void { ServerDispatch.banFromServer(userName); } - static banHistory(userName: string, banHistory: ServerInfo_Ban[]): void { + static banHistory(userName: string, banHistory: Data.ServerInfo_Ban[]): void { ServerDispatch.banHistory(userName, banHistory); } - static viewLogs(logs: ServerInfo_ChatMessage[]): void { + static viewLogs(logs: Data.ServerInfo_ChatMessage[]): void { ServerDispatch.viewLogs(logs); } - static warnHistory(userName: string, warnHistory: ServerInfo_Warning[]): void { + static warnHistory(userName: string, warnHistory: Data.ServerInfo_Warning[]): void { ServerDispatch.warnHistory(userName, warnHistory); } - static warnListOptions(warnList: Response_WarnList[]): void { + static warnListOptions(warnList: Data.Response_WarnList[]): void { ServerDispatch.warnListOptions(warnList); } diff --git a/webclient/src/websocket/persistence/RoomPersistence.spec.ts b/webclient/src/websocket/persistence/RoomPersistence.spec.ts index 61bc5e7f0..7ce780e09 100644 --- a/webclient/src/websocket/persistence/RoomPersistence.spec.ts +++ b/webclient/src/websocket/persistence/RoomPersistence.spec.ts @@ -1,4 +1,4 @@ -vi.mock('store', () => ({ +vi.mock('@app/store', () => ({ RoomsDispatch: { clearStore: vi.fn(), joinRoom: vi.fn(), @@ -15,11 +15,9 @@ vi.mock('store', () => ({ })); import { RoomPersistence } from './RoomPersistence'; -import { RoomsDispatch } from 'store'; - -beforeEach(() => { - vi.clearAllMocks(); -}); +import { RoomsDispatch } from '@app/store'; +import { create } from '@bufbuild/protobuf'; +import { Data, Enriched } from '@app/types'; describe('RoomPersistence', () => { it('clearStore -> RoomsDispatch.clearStore', () => { @@ -28,7 +26,7 @@ describe('RoomPersistence', () => { }); it('joinRoom dispatches raw roomInfo', () => { - const room = { roomId: 1 } as any; + const room = create(Data.ServerInfo_RoomSchema, { roomId: 1 }); RoomPersistence.joinRoom(room); expect(RoomsDispatch.joinRoom).toHaveBeenCalledWith(room); }); @@ -39,37 +37,37 @@ describe('RoomPersistence', () => { }); it('updateRooms dispatches raw rooms', () => { - const rooms = [{ roomId: 1 }] as any; + const rooms = [create(Data.ServerInfo_RoomSchema, { roomId: 1 })]; RoomPersistence.updateRooms(rooms); expect(RoomsDispatch.updateRooms).toHaveBeenCalledWith(rooms); }); describe('updateGames', () => { it('dispatches raw game list', () => { - const game = { gameTypes: [1] } as any; + const game = create(Data.ServerInfo_GameSchema, { gameTypes: [1] }); RoomPersistence.updateGames(1, [game]); expect(RoomsDispatch.updateGames).toHaveBeenCalledWith(1, [game]); }); it('returns without error when gameList is empty', () => { expect(() => RoomPersistence.updateGames(1, [])).not.toThrow(); - expect(RoomsDispatch.updateGames).not.toHaveBeenCalled(); + expect(RoomsDispatch.updateGames).toHaveBeenCalledWith(1, []); }); it('returns without error when gameList is null', () => { - expect(() => RoomPersistence.updateGames(1, null as any)).not.toThrow(); - expect(RoomsDispatch.updateGames).not.toHaveBeenCalled(); + expect(() => RoomPersistence.updateGames(1, null as unknown as Data.ServerInfo_Game[])).not.toThrow(); + expect(RoomsDispatch.updateGames).toHaveBeenCalledWith(1, null); }); }); it('addMessage dispatches without pre-normalizing', () => { - const msg = { name: 'alice', message: 'hi' } as any; + const msg: Enriched.Message = { ...create(Data.Event_RoomSaySchema), timeReceived: 0, name: 'alice', message: 'hi' }; RoomPersistence.addMessage(1, msg); expect(RoomsDispatch.addMessage).toHaveBeenCalledWith(1, msg); }); it('userJoined -> RoomsDispatch.userJoined', () => { - const user = { name: 'bob' } as any; + const user = create(Data.ServerInfo_UserSchema, { name: 'bob' }); RoomPersistence.userJoined(1, user); expect(RoomsDispatch.userJoined).toHaveBeenCalledWith(1, user); }); diff --git a/webclient/src/websocket/persistence/RoomPersistence.ts b/webclient/src/websocket/persistence/RoomPersistence.ts index 6325bc20a..21969598c 100644 --- a/webclient/src/websocket/persistence/RoomPersistence.ts +++ b/webclient/src/websocket/persistence/RoomPersistence.ts @@ -1,15 +1,12 @@ -import { RoomsDispatch } from 'store'; -import { Message } from 'types'; -import type { ServerInfo_User } from 'generated/proto/serverinfo_user_pb'; -import type { ServerInfo_Room } from 'generated/proto/serverinfo_room_pb'; -import type { ServerInfo_Game } from 'generated/proto/serverinfo_game_pb'; +import { RoomsDispatch } from '@app/store'; +import { Data, Enriched } from '@app/types'; export class RoomPersistence { static clearStore() { RoomsDispatch.clearStore(); } - static joinRoom(roomInfo: ServerInfo_Room) { + static joinRoom(roomInfo: Data.ServerInfo_Room) { RoomsDispatch.joinRoom(roomInfo); } @@ -17,26 +14,19 @@ export class RoomPersistence { RoomsDispatch.leaveRoom(roomId); } - static updateRooms(rooms: ServerInfo_Room[]) { + static updateRooms(rooms: Data.ServerInfo_Room[]) { RoomsDispatch.updateRooms(rooms); } - static updateGames(roomId: number, gameList: ServerInfo_Game[]) { - // Guard: the server never sends an empty gameList to signal "clear all games". - // An empty array here means no game updates — skip the dispatch to avoid - // unnecessarily overwriting the existing game list with an empty one. - if (!gameList?.length) { - return; - } - + static updateGames(roomId: number, gameList: Data.ServerInfo_Game[]) { RoomsDispatch.updateGames(roomId, gameList); } - static addMessage(roomId: number, message: Message) { + static addMessage(roomId: number, message: Enriched.Message) { RoomsDispatch.addMessage(roomId, message); } - static userJoined(roomId: number, user: ServerInfo_User) { + static userJoined(roomId: number, user: Data.ServerInfo_User) { RoomsDispatch.userJoined(roomId, user); } diff --git a/webclient/src/websocket/persistence/SessionPersistence.spec.ts b/webclient/src/websocket/persistence/SessionPersistence.spec.ts index 9dbe4a10f..16f59475c 100644 --- a/webclient/src/websocket/persistence/SessionPersistence.spec.ts +++ b/webclient/src/websocket/persistence/SessionPersistence.spec.ts @@ -1,11 +1,10 @@ -vi.mock('store', () => ({ +vi.mock('@app/store', () => ({ ServerDispatch: { initialized: vi.fn(), connectionAttempted: vi.fn(), clearStore: vi.fn(), loginSuccessful: vi.fn(), loginFailed: vi.fn(), - connectionClosed: vi.fn(), connectionFailed: vi.fn(), testConnectionSuccessful: vi.fn(), testConnectionFailed: vi.fn(), @@ -61,18 +60,19 @@ vi.mock('store', () => ({ }, })); -vi.mock('websocket/utils', () => ({ +vi.mock('../utils', () => ({ sanitizeHtml: vi.fn((msg: string) => `sanitized:${msg}`), })); import { SessionPersistence } from './SessionPersistence'; -import { ServerDispatch, GameDispatch } from 'store'; -import { sanitizeHtml } from 'websocket/utils'; -import { StatusEnum } from 'types'; +import { ServerDispatch, GameDispatch } from '@app/store'; +import { sanitizeHtml } from '../utils'; +import { App, Data, Enriched } from '@app/types'; +import { create } from '@bufbuild/protobuf'; + import { Mock } from 'vitest'; beforeEach(() => { - vi.clearAllMocks(); (sanitizeHtml as Mock).mockImplementation((msg: string) => `sanitized:${msg}`); }); @@ -88,7 +88,7 @@ describe('SessionPersistence', () => { }); it('loginSuccessful passes options', () => { - const opts = { userName: 'alice' } as any; + const opts: Enriched.LoginSuccessContext = { hashedPassword: 'hash' }; SessionPersistence.loginSuccessful(opts); expect(ServerDispatch.loginSuccessful).toHaveBeenCalledWith(opts); }); @@ -98,11 +98,6 @@ describe('SessionPersistence', () => { expect(ServerDispatch.loginFailed).toHaveBeenCalled(); }); - it('connectionClosed passes reason', () => { - SessionPersistence.connectionClosed(3); - expect(ServerDispatch.connectionClosed).toHaveBeenCalledWith(3); - }); - it('connectionFailed -> ServerDispatch.connectionFailed', () => { SessionPersistence.connectionFailed(); expect(ServerDispatch.connectionFailed).toHaveBeenCalled(); @@ -124,7 +119,7 @@ describe('SessionPersistence', () => { }); it('addToBuddyList passes user', () => { - const user = { name: 'bob' } as any; + const user = create(Data.ServerInfo_UserSchema, { name: 'bob' }); SessionPersistence.addToBuddyList(user); expect(ServerDispatch.addToBuddyList).toHaveBeenCalledWith(user); }); @@ -140,7 +135,7 @@ describe('SessionPersistence', () => { }); it('addToIgnoreList passes user', () => { - const user = { name: 'bob' } as any; + const user = create(Data.ServerInfo_UserSchema, { name: 'bob' }); SessionPersistence.addToIgnoreList(user); expect(ServerDispatch.addToIgnoreList).toHaveBeenCalledWith(user); }); @@ -155,19 +150,13 @@ describe('SessionPersistence', () => { expect(ServerDispatch.updateInfo).toHaveBeenCalledWith('Server', '1.0'); }); - it('updateStatus dispatches status and calls connectionClosed when DISCONNECTED', () => { - SessionPersistence.updateStatus(StatusEnum.DISCONNECTED, 'bye'); - expect(ServerDispatch.updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'bye'); - expect(ServerDispatch.connectionClosed).toHaveBeenCalledWith(StatusEnum.DISCONNECTED); - }); - - it('updateStatus does not call connectionClosed when not DISCONNECTED', () => { - SessionPersistence.updateStatus(StatusEnum.CONNECTED, 'hi'); - expect(ServerDispatch.connectionClosed).not.toHaveBeenCalled(); + it('updateStatus passes state and description', () => { + SessionPersistence.updateStatus(App.StatusEnum.DISCONNECTED, 'bye'); + expect(ServerDispatch.updateStatus).toHaveBeenCalledWith(App.StatusEnum.DISCONNECTED, 'bye'); }); it('updateUser passes user', () => { - const user = { name: 'alice' } as any; + const user = create(Data.ServerInfo_UserSchema, { name: 'alice' }); SessionPersistence.updateUser(user); expect(ServerDispatch.updateUser).toHaveBeenCalledWith(user); }); @@ -178,7 +167,7 @@ describe('SessionPersistence', () => { }); it('userJoined passes user', () => { - const user = { name: 'carol' } as any; + const user = create(Data.ServerInfo_UserSchema, { name: 'carol' }); SessionPersistence.userJoined(user); expect(ServerDispatch.userJoined).toHaveBeenCalledWith(user); }); @@ -195,7 +184,7 @@ describe('SessionPersistence', () => { }); it('accountAwaitingActivation passes options', () => { - const opts = { userName: 'u' } as any; + const opts: Enriched.PendingActivationContext = { host: 'h', port: '1', userName: 'u' }; SessionPersistence.accountAwaitingActivation(opts); expect(ServerDispatch.accountAwaitingActivation).toHaveBeenCalledWith(opts); }); @@ -282,53 +271,55 @@ describe('SessionPersistence', () => { }); it('getUserInfo passes userInfo', () => { - const user = { name: 'u' } as any; + const user = create(Data.ServerInfo_UserSchema, { name: 'u' }); SessionPersistence.getUserInfo(user); expect(ServerDispatch.getUserInfo).toHaveBeenCalledWith(user); }); - it('getGamesOfUser builds gametypeMap and dispatches raw games with map', () => { - const gt = { gameTypeId: 1, description: 'Standard' }; - const room = { gametypeList: [gt] }; - const game = { gameId: 5, roomId: 1, gameTypes: [1], description: 'My Game', started: false }; - SessionPersistence.getGamesOfUser('alice', { roomList: [room], gameList: [game] } as any); - expect(ServerDispatch.gamesOfUser).toHaveBeenCalledWith('alice', [game], { 1: 'Standard' }); + it('getGamesOfUser passes response to ServerDispatch', () => { + const response = create(Data.Response_GetGamesOfUserSchema, { + roomList: [create(Data.ServerInfo_RoomSchema, { + gametypeList: [create(Data.ServerInfo_GameTypeSchema, { gameTypeId: 1, description: 'Standard' })] + })], + gameList: [create(Data.ServerInfo_GameSchema, { gameId: 5 })], + }); + SessionPersistence.getGamesOfUser('alice', response); + expect(ServerDispatch.gamesOfUser).toHaveBeenCalledWith('alice', response); }); it('getGamesOfUser handles empty response', () => { - SessionPersistence.getGamesOfUser('alice', {} as any); - expect(ServerDispatch.gamesOfUser).toHaveBeenCalledWith('alice', [], {}); + const emptyResponse = create(Data.Response_GetGamesOfUserSchema, {}); + SessionPersistence.getGamesOfUser('alice', emptyResponse); + expect(ServerDispatch.gamesOfUser).toHaveBeenCalledWith('alice', emptyResponse); }); - it('gameJoined dispatches via GameDispatch.gameJoined', () => { - const gameInfo = { gameId: 10, roomId: 2, description: 'test', started: false }; - SessionPersistence.gameJoined({ gameInfo, hostId: 3, playerId: 4, spectator: false, judge: false, resuming: true } as any); - expect(GameDispatch.gameJoined).toHaveBeenCalledWith( - 10, - expect.objectContaining({ gameId: 10, hostId: 3, localPlayerId: 4, resuming: true }) - ); + it('gameJoined dispatches raw event via GameDispatch.gameJoined', () => { + const gameInfo = create(Data.ServerInfo_GameSchema, { gameId: 10, roomId: 2, description: 'test', started: false }); + const data = create(Data.Event_GameJoinedSchema, { gameInfo, hostId: 3, playerId: 4, spectator: false, judge: false, resuming: true }); + SessionPersistence.gameJoined(data); + expect(GameDispatch.gameJoined).toHaveBeenCalledWith(data); }); it('notifyUser passes notification', () => { - const notif = { type: 1 } as any; + const notif = create(Data.Event_NotifyUserSchema, { type: 1 }); SessionPersistence.notifyUser(notif); expect(ServerDispatch.notifyUser).toHaveBeenCalledWith(notif); }); it('playerPropertiesChanged dispatches via GameDispatch', () => { - const props = { pingTime: 100 }; - SessionPersistence.playerPropertiesChanged(5, 1, { playerProperties: props } as any); + const props = create(Data.ServerInfo_PlayerPropertiesSchema, { pingTime: 100 }); + SessionPersistence.playerPropertiesChanged(5, 1, create(Data.Event_PlayerPropertiesChangedSchema, { playerProperties: props })); expect(GameDispatch.playerPropertiesChanged).toHaveBeenCalledWith(5, 1, props); }); it('serverShutdown passes data', () => { - const data = { gracePeriod: 5 } as any; + const data = create(Data.Event_ServerShutdownSchema, { gracePeriod: 5 }); SessionPersistence.serverShutdown(data); expect(ServerDispatch.serverShutdown).toHaveBeenCalledWith(data); }); it('userMessage passes messageData', () => { - const msg = { message: 'hello' } as any; + const msg = create(Data.Event_UserMessageSchema, { message: 'hello' }); SessionPersistence.userMessage(msg); expect(ServerDispatch.userMessage).toHaveBeenCalledWith(msg); }); @@ -349,13 +340,14 @@ describe('SessionPersistence', () => { }); it('updateServerDecks passes deckList', () => { - SessionPersistence.updateServerDecks({ folders: [] } as any); + SessionPersistence.updateServerDecks(create(Data.Response_DeckListSchema, { folders: [] })); expect(ServerDispatch.backendDecks).toHaveBeenCalled(); }); it('uploadServerDeck passes path and treeItem', () => { - SessionPersistence.uploadServerDeck('/path', { id: 1 } as any); - expect(ServerDispatch.deckUpload).toHaveBeenCalledWith('/path', { id: 1 }); + const treeItem = create(Data.ServerInfo_DeckStorage_TreeItemSchema, { id: 1 }); + SessionPersistence.uploadServerDeck('/path', treeItem); + expect(ServerDispatch.deckUpload).toHaveBeenCalledWith('/path', treeItem); }); it('createServerDeckDir passes path and dirName', () => { @@ -374,7 +366,7 @@ describe('SessionPersistence', () => { }); it('replayAdded passes matchInfo', () => { - const match = { gameId: 1 } as any; + const match = create(Data.ServerInfo_ReplayMatchSchema, { gameId: 1 }); SessionPersistence.replayAdded(match); expect(ServerDispatch.replayAdded).toHaveBeenCalledWith(match); }); @@ -395,14 +387,15 @@ describe('SessionPersistence', () => { }); it('playerPropertiesChanged does nothing when payload has no playerProperties', () => { - SessionPersistence.playerPropertiesChanged(5, 1, {} as any); + SessionPersistence.playerPropertiesChanged(5, 1, create(Data.Event_PlayerPropertiesChangedSchema, {})); expect(GameDispatch.playerPropertiesChanged).not.toHaveBeenCalled(); }); it('getGamesOfUser handles rooms with missing gametypeList', () => { - const room = {} as any; - const game = { gameId: 5 }; - SessionPersistence.getGamesOfUser('alice', { roomList: [room], gameList: [game] } as any); - expect(ServerDispatch.gamesOfUser).toHaveBeenCalledWith('alice', [game], {}); + const room = create(Data.ServerInfo_RoomSchema, {}); + const game = create(Data.ServerInfo_GameSchema, { gameId: 5 }); + const response = create(Data.Response_GetGamesOfUserSchema, { roomList: [room], gameList: [game] }); + SessionPersistence.getGamesOfUser('alice', response); + expect(ServerDispatch.gamesOfUser).toHaveBeenCalledWith('alice', response); }); }); diff --git a/webclient/src/websocket/persistence/SessionPersistence.ts b/webclient/src/websocket/persistence/SessionPersistence.ts index 84b5f7b1c..d2071fc1f 100644 --- a/webclient/src/websocket/persistence/SessionPersistence.ts +++ b/webclient/src/websocket/persistence/SessionPersistence.ts @@ -1,22 +1,6 @@ -import { GameDispatch, ServerDispatch } from 'store'; -import { StatusEnum, WebSocketConnectOptions } from 'types'; -import type { ServerInfo_User } from 'generated/proto/serverinfo_user_pb'; -import type { Response_DeckList } from 'generated/proto/response_deck_list_pb'; -import type { ServerInfo_DeckStorage_TreeItem } from 'generated/proto/serverinfo_deckstorage_pb'; -import type { ServerInfo_ReplayMatch } from 'generated/proto/serverinfo_replay_match_pb'; -import { GameEntry } from 'store/game/game.interfaces'; -import { sanitizeHtml } from 'websocket/utils'; -import { - GameJoinedData, - NotifyUserData, - PlayerGamePropertiesData, - ServerShutdownData, - UserMessageData -} from '../events/session/interfaces'; - -import type { Response_GetGamesOfUser } from 'generated/proto/response_get_games_of_user_pb'; -import type { ServerInfo_Room } from 'generated/proto/serverinfo_room_pb'; -import type { ServerInfo_GameType } from 'generated/proto/serverinfo_gametype_pb'; +import { GameDispatch, ServerDispatch } from '@app/store'; +import { App, Data, Enriched } from '@app/types'; +import { sanitizeHtml } from '../utils'; export class SessionPersistence { static initialized() { @@ -31,7 +15,7 @@ export class SessionPersistence { ServerDispatch.clearStore(); } - static loginSuccessful(options: WebSocketConnectOptions) { + static loginSuccessful(options: Enriched.LoginSuccessContext) { ServerDispatch.loginSuccessful(options); } @@ -39,10 +23,6 @@ export class SessionPersistence { ServerDispatch.loginFailed(); } - static connectionClosed(reason: number) { - ServerDispatch.connectionClosed(reason); - } - static connectionFailed() { ServerDispatch.connectionFailed(); } @@ -55,11 +35,11 @@ export class SessionPersistence { ServerDispatch.testConnectionFailed(); } - static updateBuddyList(buddyList: ServerInfo_User[]) { + static updateBuddyList(buddyList: Data.ServerInfo_User[]) { ServerDispatch.updateBuddyList(buddyList); } - static addToBuddyList(user: ServerInfo_User) { + static addToBuddyList(user: Data.ServerInfo_User) { ServerDispatch.addToBuddyList(user); } @@ -67,11 +47,11 @@ export class SessionPersistence { ServerDispatch.removeFromBuddyList(userName); } - static updateIgnoreList(ignoreList: ServerInfo_User[]) { + static updateIgnoreList(ignoreList: Data.ServerInfo_User[]) { ServerDispatch.updateIgnoreList(ignoreList); } - static addToIgnoreList(user: ServerInfo_User) { + static addToIgnoreList(user: Data.ServerInfo_User) { ServerDispatch.addToIgnoreList(user); } @@ -83,23 +63,19 @@ export class SessionPersistence { ServerDispatch.updateInfo(name, version); } - static updateStatus(state: number, description: string) { + static updateStatus(state: App.StatusEnum, description: string) { ServerDispatch.updateStatus(state, description); - - if (state === StatusEnum.DISCONNECTED) { - this.connectionClosed(state); - } } - static updateUser(user: ServerInfo_User) { + static updateUser(user: Data.ServerInfo_User) { ServerDispatch.updateUser(user); } - static updateUsers(users: ServerInfo_User[]) { + static updateUsers(users: Data.ServerInfo_User[]) { ServerDispatch.updateUsers(users); } - static userJoined(user: ServerInfo_User) { + static userJoined(user: Data.ServerInfo_User) { ServerDispatch.userJoined(user); } @@ -111,7 +87,7 @@ export class SessionPersistence { ServerDispatch.serverMessage(sanitizeHtml(message)); } - static accountAwaitingActivation(options: WebSocketConnectOptions) { + static accountAwaitingActivation(options: Enriched.PendingActivationContext) { ServerDispatch.accountAwaitingActivation(options); } @@ -175,58 +151,33 @@ export class SessionPersistence { ServerDispatch.accountImageChanged({ avatarBmp }); } - static getUserInfo(userInfo: ServerInfo_User) { + static getUserInfo(userInfo: Data.ServerInfo_User) { ServerDispatch.getUserInfo(userInfo); } - static getGamesOfUser(userName: string, response: Response_GetGamesOfUser): void { - const gametypeMap: Record = {}; - (response.roomList || []).forEach((room: ServerInfo_Room) => { - (room.gametypeList || []).forEach((gt: ServerInfo_GameType) => { - gametypeMap[gt.gameTypeId] = gt.description; - }); - }); - const games = response.gameList || []; - ServerDispatch.gamesOfUser(userName, games, gametypeMap); + static getGamesOfUser(userName: string, response: Data.Response_GetGamesOfUser): void { + ServerDispatch.gamesOfUser(userName, response); } - static gameJoined(gameJoinedData: GameJoinedData): void { - const { gameInfo, hostId, playerId, spectator, judge, resuming } = gameJoinedData; - const gameEntry: GameEntry = { - gameId: gameInfo.gameId, - roomId: gameInfo.roomId, - description: gameInfo.description, - hostId, - localPlayerId: playerId, - spectator, - judge, - resuming, - started: gameInfo.started, - activePlayerId: -1, - activePhase: -1, - secondsElapsed: 0, - reversed: false, - players: {}, - messages: [], - }; - GameDispatch.gameJoined(gameInfo.gameId, gameEntry); + static gameJoined(gameJoinedData: Data.Event_GameJoined): void { + GameDispatch.gameJoined(gameJoinedData); } - static notifyUser(notification: NotifyUserData): void { + static notifyUser(notification: Data.Event_NotifyUser): void { ServerDispatch.notifyUser(notification); } - static playerPropertiesChanged(gameId: number, playerId: number, payload: PlayerGamePropertiesData): void { + static playerPropertiesChanged(gameId: number, playerId: number, payload: Data.Event_PlayerPropertiesChanged): void { if (payload.playerProperties) { GameDispatch.playerPropertiesChanged(gameId, playerId, payload.playerProperties); } } - static serverShutdown(data: ServerShutdownData): void { + static serverShutdown(data: Data.Event_ServerShutdown): void { ServerDispatch.serverShutdown(data); } - static userMessage(messageData: UserMessageData): void { + static userMessage(messageData: Data.Event_UserMessage): void { ServerDispatch.userMessage(messageData); } @@ -242,11 +193,11 @@ export class SessionPersistence { ServerDispatch.deckDelete(deckId); } - static updateServerDecks(deckList: Response_DeckList): void { + static updateServerDecks(deckList: Data.Response_DeckList): void { ServerDispatch.backendDecks(deckList); } - static uploadServerDeck(path: string, treeItem: ServerInfo_DeckStorage_TreeItem): void { + static uploadServerDeck(path: string, treeItem: Data.ServerInfo_DeckStorage_TreeItem): void { ServerDispatch.deckUpload(path, treeItem); } @@ -258,11 +209,11 @@ export class SessionPersistence { ServerDispatch.deckDelDir(path); } - static replayList(matchList: ServerInfo_ReplayMatch[]): void { + static replayList(matchList: Data.ServerInfo_ReplayMatch[]): void { ServerDispatch.replayList(matchList); } - static replayAdded(matchInfo: ServerInfo_ReplayMatch): void { + static replayAdded(matchInfo: Data.ServerInfo_ReplayMatch): void { ServerDispatch.replayAdded(matchInfo); } diff --git a/webclient/src/websocket/services/KeepAliveService.spec.ts b/webclient/src/websocket/services/KeepAliveService.spec.ts index 4d45d6305..644bb588b 100644 --- a/webclient/src/websocket/services/KeepAliveService.spec.ts +++ b/webclient/src/websocket/services/KeepAliveService.spec.ts @@ -1,6 +1,11 @@ import { KeepAliveService } from './KeepAliveService'; import { WebSocketService } from './WebSocketService'; +type KeepAliveInternal = KeepAliveService & { + keepalivecb: NodeJS.Timeout; + lastPingPending: boolean; +}; + vi.mock('./WebSocketService'); describe('KeepAliveService', () => { @@ -38,15 +43,15 @@ describe('KeepAliveService', () => { }); it('should start ping loop', () => { - expect((service as any).keepalivecb).toBeDefined(); - expect((service as any).lastPingPending).toBeTruthy(); + expect((service as KeepAliveInternal).keepalivecb).toBeDefined(); + expect((service as KeepAliveInternal).lastPingPending).toBeTruthy(); }); it('should call ping callback when done', () => { resolvePing(); return promise.then(() => { - expect((service as any).lastPingPending).toBeFalsy(); + expect((service as KeepAliveInternal).lastPingPending).toBeFalsy(); }); }); diff --git a/webclient/src/websocket/services/ProtobufService.spec.ts b/webclient/src/websocket/services/ProtobufService.spec.ts index 4a4ec401d..0c578de77 100644 --- a/webclient/src/websocket/services/ProtobufService.spec.ts +++ b/webclient/src/websocket/services/ProtobufService.spec.ts @@ -1,5 +1,5 @@ vi.mock('@bufbuild/protobuf', () => ({ - create: vi.fn((_schema: any, fields?: any) => ({ ...(fields ?? {}) })), + create: vi.fn((_schema: unknown, fields?: Record) => ({ ...(fields ?? {}) })), fromBinary: vi.fn(), toBinary: vi.fn().mockReturnValue(new Uint8Array()), hasExtension: vi.fn().mockReturnValue(false), @@ -7,11 +7,11 @@ vi.mock('@bufbuild/protobuf', () => ({ setExtension: vi.fn(), })); -vi.mock('generated/proto/commands_pb', () => ({ +vi.mock('../../generated/proto/commands_pb', () => ({ CommandContainerSchema: {}, })); -vi.mock('generated/proto/server_message_pb', () => ({ +vi.mock('../../generated/proto/server_message_pb', () => ({ ServerMessageSchema: {}, ServerMessage_MessageType: { RESPONSE: 1, @@ -32,16 +32,29 @@ vi.mock('../WebClient', () => ({ default: {}, })); -import { fromBinary, hasExtension, getExtension } from '@bufbuild/protobuf'; -import { ServerMessage_MessageType } from 'generated/proto/server_message_pb'; +import { create, fromBinary, hasExtension, getExtension } from '@bufbuild/protobuf'; +import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; + import { ProtobufService } from './ProtobufService'; import { GameEvents, RoomEvents, SessionEvents } from '../events'; +import type { GameExtensionRegistry } from '../events/game'; +import type { RoomExtensionRegistry } from '../events/room'; +import type { SessionExtensionRegistry } from '../events/session'; -let mockSocket: any; +import { Data } from '@app/types'; + +type ProtobufInternal = ProtobufService & { + cmdId: number; + pendingCommands: Map void>; + processGameEvent(container: unknown, extra?: unknown): void; + processRoomEvent(event: unknown): void; + processSessionEvent(event: unknown): void; + processServerResponse(response: unknown): void; +}; + +let mockSocket: { isOpen: ReturnType; send: ReturnType }; beforeEach(() => { - vi.clearAllMocks(); - mockSocket = { isOpen: vi.fn().mockReturnValue(true), send: vi.fn(), @@ -49,14 +62,21 @@ beforeEach(() => { }); describe('ProtobufService', () => { + // Mock extensions for send*Command tests — @bufbuild/protobuf is fully mocked so these are never invoked + const sessionExt = {} as GenExtension>; + const roomExt = {} as GenExtension>; + const gameExt = {} as GenExtension>; + const moderatorExt = {} as GenExtension>; + const adminExt = {} as GenExtension>; + describe('resetCommands', () => { it('resets cmdId and pendingCommands', () => { const service = new ProtobufService(mockSocket); - service.sendSessionCommand({} as any, vi.fn()); - expect((service as any).cmdId).toBe(1); + service.sendSessionCommand(sessionExt, vi.fn()); + expect((service as ProtobufInternal).cmdId).toBe(1); service.resetCommands(); - expect((service as any).cmdId).toBe(0); - expect((service as any).pendingCommands).toEqual(new Map()); + expect((service as ProtobufInternal).cmdId).toBe(0); + expect((service as ProtobufInternal).pendingCommands).toEqual(new Map()); }); }); @@ -64,22 +84,22 @@ describe('ProtobufService', () => { it('increments cmdId and stores callback', () => { const service = new ProtobufService(mockSocket); const cb = vi.fn(); - service.sendCommand({} as any, cb); - expect((service as any).cmdId).toBe(1); - expect((service as any).pendingCommands.get(1)).toBe(cb); + service.sendCommand(create(Data.CommandContainerSchema), cb); + expect((service as ProtobufInternal).cmdId).toBe(1); + expect((service as ProtobufInternal).pendingCommands.get(1)).toBe(cb); }); it('sends encoded data when socket is OPEN', () => { const service = new ProtobufService(mockSocket); mockSocket.isOpen.mockReturnValue(true); - service.sendCommand({} as any, vi.fn()); + service.sendCommand(create(Data.CommandContainerSchema), vi.fn()); expect(mockSocket.send).toHaveBeenCalled(); }); it('does not send when socket is not OPEN', () => { const service = new ProtobufService(mockSocket); mockSocket.isOpen.mockReturnValue(false); - service.sendCommand({} as any, vi.fn()); + service.sendCommand(create(Data.CommandContainerSchema), vi.fn()); expect(mockSocket.send).not.toHaveBeenCalled(); }); }); @@ -87,136 +107,136 @@ describe('ProtobufService', () => { describe('sendSessionCommand', () => { it('stores callback and increments cmdId', () => { const service = new ProtobufService(mockSocket); - service.sendSessionCommand({} as any, {}); - expect((service as any).cmdId).toBe(1); - expect((service as any).pendingCommands.get(1)).toBeTypeOf('function'); + service.sendSessionCommand(sessionExt, {}); + expect((service as ProtobufInternal).cmdId).toBe(1); + expect((service as ProtobufInternal).pendingCommands.get(1)).toBeTypeOf('function'); }); it('invokes onResponse with raw response when the pending command is triggered', () => { const service = new ProtobufService(mockSocket); const cb = vi.fn(); - service.sendSessionCommand({} as any, {}, { onResponse: cb }); + service.sendSessionCommand(sessionExt, {}, { onResponse: cb }); - const storedCb = (service as any).pendingCommands.get(1); - storedCb({ responseData: true }); + const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; + storedCb(create(Data.ResponseSchema)); - expect(cb).toHaveBeenCalledWith({ responseData: true }); + expect(cb).toHaveBeenCalledWith(create(Data.ResponseSchema)); }); it('does not throw when no callback is provided and pending command is triggered', () => { const service = new ProtobufService(mockSocket); - service.sendSessionCommand({} as any, {}); + service.sendSessionCommand(sessionExt, {}); - const storedCb = (service as any).pendingCommands.get(1); - expect(() => storedCb({ responseData: true })).not.toThrow(); + const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; + expect(() => storedCb(create(Data.ResponseSchema))).not.toThrow(); }); }); describe('sendRoomCommand', () => { it('stores callback and increments cmdId', () => { const service = new ProtobufService(mockSocket); - service.sendRoomCommand(42, {} as any, {}); - expect((service as any).cmdId).toBe(1); + service.sendRoomCommand(42, roomExt, {}); + expect((service as ProtobufInternal).cmdId).toBe(1); }); it('invokes onResponse with raw response when the pending command is triggered', () => { const service = new ProtobufService(mockSocket); const cb = vi.fn(); - service.sendRoomCommand(42, {} as any, {}, { onResponse: cb }); + service.sendRoomCommand(42, roomExt, {}, { onResponse: cb }); - const storedCb = (service as any).pendingCommands.get(1); - storedCb({ responseData: true }); + const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; + storedCb(create(Data.ResponseSchema)); - expect(cb).toHaveBeenCalledWith({ responseData: true }); + expect(cb).toHaveBeenCalledWith(create(Data.ResponseSchema)); }); it('does not throw when no callback is provided and pending command is triggered', () => { const service = new ProtobufService(mockSocket); - service.sendRoomCommand(42, {} as any, {}); + service.sendRoomCommand(42, roomExt, {}); - const storedCb = (service as any).pendingCommands.get(1); - expect(() => storedCb({ responseData: true })).not.toThrow(); + const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; + expect(() => storedCb(create(Data.ResponseSchema))).not.toThrow(); }); }); describe('sendGameCommand', () => { it('stores callback and increments cmdId', () => { const service = new ProtobufService(mockSocket); - service.sendGameCommand(7, {} as any, {}); - expect((service as any).cmdId).toBe(1); + service.sendGameCommand(7, gameExt, {}); + expect((service as ProtobufInternal).cmdId).toBe(1); }); it('invokes onResponse with raw response when the pending command is triggered', () => { const service = new ProtobufService(mockSocket); const cb = vi.fn(); - service.sendGameCommand(7, {} as any, {}, { onResponse: cb }); + service.sendGameCommand(7, gameExt, {}, { onResponse: cb }); - const storedCb = (service as any).pendingCommands.get(1); - storedCb({ responseData: true }); + const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; + storedCb(create(Data.ResponseSchema)); - expect(cb).toHaveBeenCalledWith({ responseData: true }); + expect(cb).toHaveBeenCalledWith(create(Data.ResponseSchema)); }); it('does not throw when no callback is provided and pending command is triggered', () => { const service = new ProtobufService(mockSocket); - service.sendGameCommand(7, {} as any, {}); + service.sendGameCommand(7, gameExt, {}); - const storedCb = (service as any).pendingCommands.get(1); - expect(() => storedCb({ responseData: true })).not.toThrow(); + const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; + expect(() => storedCb(create(Data.ResponseSchema))).not.toThrow(); }); }); describe('sendModeratorCommand', () => { it('stores callback and increments cmdId', () => { const service = new ProtobufService(mockSocket); - service.sendModeratorCommand({} as any, {}); - expect((service as any).cmdId).toBe(1); + service.sendModeratorCommand(moderatorExt, {}); + expect((service as ProtobufInternal).cmdId).toBe(1); }); it('invokes onResponse with raw response when the pending command is triggered', () => { const service = new ProtobufService(mockSocket); const cb = vi.fn(); - service.sendModeratorCommand({} as any, {}, { onResponse: cb }); + service.sendModeratorCommand(moderatorExt, {}, { onResponse: cb }); - const storedCb = (service as any).pendingCommands.get(1); - storedCb({ responseData: true }); + const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; + storedCb(create(Data.ResponseSchema)); - expect(cb).toHaveBeenCalledWith({ responseData: true }); + expect(cb).toHaveBeenCalledWith(create(Data.ResponseSchema)); }); it('does not throw when no callback is provided and pending command is triggered', () => { const service = new ProtobufService(mockSocket); - service.sendModeratorCommand({} as any, {}); + service.sendModeratorCommand(moderatorExt, {}); - const storedCb = (service as any).pendingCommands.get(1); - expect(() => storedCb({ responseData: true })).not.toThrow(); + const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; + expect(() => storedCb(create(Data.ResponseSchema))).not.toThrow(); }); }); describe('sendAdminCommand', () => { it('stores callback and increments cmdId', () => { const service = new ProtobufService(mockSocket); - service.sendAdminCommand({} as any, {}); - expect((service as any).cmdId).toBe(1); + service.sendAdminCommand(adminExt, {}); + expect((service as ProtobufInternal).cmdId).toBe(1); }); it('invokes onResponse with raw response when the pending command is triggered', () => { const service = new ProtobufService(mockSocket); const cb = vi.fn(); - service.sendAdminCommand({} as any, {}, { onResponse: cb }); + service.sendAdminCommand(adminExt, {}, { onResponse: cb }); - const storedCb = (service as any).pendingCommands.get(1); - storedCb({ responseData: true }); + const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; + storedCb(create(Data.ResponseSchema)); - expect(cb).toHaveBeenCalledWith({ responseData: true }); + expect(cb).toHaveBeenCalledWith(create(Data.ResponseSchema)); }); it('does not throw when no callback is provided and pending command is triggered', () => { const service = new ProtobufService(mockSocket); - service.sendAdminCommand({} as any, {}); + service.sendAdminCommand(adminExt, {}); - const storedCb = (service as any).pendingCommands.get(1); - expect(() => storedCb({ responseData: true })).not.toThrow(); + const storedCb = (service as ProtobufInternal).pendingCommands.get(1)!; + expect(() => storedCb(create(Data.ResponseSchema))).not.toThrow(); }); }); @@ -224,27 +244,30 @@ describe('ProtobufService', () => { it('routes RESPONSE message to processServerResponse', () => { const service = new ProtobufService(mockSocket); const cb = vi.fn(); - (service as any).cmdId = 1; - (service as any).pendingCommands.set(1, cb); + (service as ProtobufInternal).cmdId = 1; + (service as ProtobufInternal).pendingCommands.set(1, cb); - vi.mocked(fromBinary).mockReturnValue({ - messageType: ServerMessage_MessageType.RESPONSE, - response: { cmdId: BigInt(1) }, - } as any); + vi.mocked(fromBinary).mockReturnValue( + create(Data.ServerMessageSchema, { + messageType: Data.ServerMessage_MessageType.RESPONSE, + response: create(Data.ResponseSchema, { cmdId: BigInt(1) }), + }) + ); service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent); - expect(cb).toHaveBeenCalledWith({ cmdId: BigInt(1) }); - expect((service as any).pendingCommands.get(1)).toBeUndefined(); + expect(cb).toHaveBeenCalledWith(expect.objectContaining({ cmdId: BigInt(1) })); + expect((service as ProtobufInternal).pendingCommands.get(1)).toBeUndefined(); }); it('routes ROOM_EVENT message', () => { const service = new ProtobufService(mockSocket); - const processRoomEvent = vi.spyOn(service as any, 'processRoomEvent'); + const processRoomEvent = vi.spyOn(service as ProtobufInternal, 'processRoomEvent'); - vi.mocked(fromBinary).mockReturnValue({ - messageType: ServerMessage_MessageType.ROOM_EVENT, - roomEvent: {}, - } as any); + vi.mocked(fromBinary).mockReturnValue( + create(Data.ServerMessageSchema, { + messageType: Data.ServerMessage_MessageType.ROOM_EVENT, + }) + ); service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent); expect(processRoomEvent).toHaveBeenCalled(); @@ -252,12 +275,13 @@ describe('ProtobufService', () => { it('routes SESSION_EVENT message', () => { const service = new ProtobufService(mockSocket); - const processSessionEvent = vi.spyOn(service as any, 'processSessionEvent'); + const processSessionEvent = vi.spyOn(service as ProtobufInternal, 'processSessionEvent'); - vi.mocked(fromBinary).mockReturnValue({ - messageType: ServerMessage_MessageType.SESSION_EVENT, - sessionEvent: {}, - } as any); + vi.mocked(fromBinary).mockReturnValue( + create(Data.ServerMessageSchema, { + messageType: Data.ServerMessage_MessageType.SESSION_EVENT, + }) + ); service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent); expect(processSessionEvent).toHaveBeenCalled(); @@ -265,12 +289,13 @@ describe('ProtobufService', () => { it('routes GAME_EVENT_CONTAINER message', () => { const service = new ProtobufService(mockSocket); - const processGameEvent = vi.spyOn(service as any, 'processGameEvent'); + const processGameEvent = vi.spyOn(service as ProtobufInternal, 'processGameEvent'); - vi.mocked(fromBinary).mockReturnValue({ - messageType: ServerMessage_MessageType.GAME_EVENT_CONTAINER, - gameEventContainer: {}, - } as any); + vi.mocked(fromBinary).mockReturnValue( + create(Data.ServerMessageSchema, { + messageType: Data.ServerMessage_MessageType.GAME_EVENT_CONTAINER, + }) + ); service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent); expect(processGameEvent).toHaveBeenCalled(); @@ -280,9 +305,11 @@ describe('ProtobufService', () => { const service = new ProtobufService(mockSocket); const consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); - vi.mocked(fromBinary).mockReturnValue({ - messageType: 999, - } as any); + vi.mocked(fromBinary).mockReturnValue( + create(Data.ServerMessageSchema, { + messageType: 999, + }) + ); service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent); expect(consoleSpy).toHaveBeenCalled(); @@ -291,7 +318,7 @@ describe('ProtobufService', () => { it('does nothing when decoded message is null', () => { const service = new ProtobufService(mockSocket); - vi.mocked(fromBinary).mockReturnValue(null as any); + vi.mocked(fromBinary).mockReturnValue(null!); expect(() => service.handleMessageEvent({ data: new ArrayBuffer(0) } as MessageEvent)).not.toThrow(); }); @@ -311,56 +338,56 @@ describe('ProtobufService', () => { it('returns early when container has no eventList', () => { const service = new ProtobufService(mockSocket); vi.mocked(hasExtension).mockReturnValue(false); - (service as any).processGameEvent(null, {}); + (service as ProtobufInternal).processGameEvent(null, {}); expect(hasExtension).not.toHaveBeenCalled(); }); it('dispatches to a GameEvents handler when hasExtension returns true', () => { const service = new ProtobufService(mockSocket); const handler = vi.fn(); - const mockExt = {}; + const mockExt = {} as GenExtension; const payload = { someData: 1 }; // Temporarily override GameEvents for this test - (GameEvents as any).push([mockExt, handler]); + (GameEvents as GameExtensionRegistry).push([mockExt, handler]); vi.mocked(hasExtension).mockReturnValue(true); vi.mocked(getExtension).mockReturnValue(payload); - (service as any).processGameEvent({ + (service as ProtobufInternal).processGameEvent({ gameId: 42, eventList: [{ playerId: 5 }], }, {}); expect(handler).toHaveBeenCalledWith(payload, expect.objectContaining({ gameId: 42, playerId: 5 })); - (GameEvents as any).pop(); + (GameEvents as GameExtensionRegistry).pop(); }); it('defaults gameId and playerId to -1 when undefined', () => { const service = new ProtobufService(mockSocket); const handler = vi.fn(); - const mockExt = {}; + const mockExt = {} as GenExtension; const payload = { someData: 1 }; - (GameEvents as any).push([mockExt, handler]); + (GameEvents as GameExtensionRegistry).push([mockExt, handler]); vi.mocked(hasExtension).mockReturnValue(true); vi.mocked(getExtension).mockReturnValue(payload); - (service as any).processGameEvent({ + (service as ProtobufInternal).processGameEvent({ gameId: undefined, eventList: [{ playerId: undefined }], }); expect(handler).toHaveBeenCalledWith(payload, expect.objectContaining({ gameId: -1, playerId: -1 })); - (GameEvents as any).pop(); + (GameEvents as GameExtensionRegistry).pop(); }); }); describe('processServerResponse', () => { it('returns early when response is undefined', () => { const service = new ProtobufService(mockSocket); - (service as any).pendingCommands.set(1, vi.fn()); - (service as any).processServerResponse(undefined); - expect((service as any).pendingCommands.size).toBe(1); + (service as ProtobufInternal).pendingCommands.set(1, vi.fn()); + (service as ProtobufInternal).processServerResponse(undefined); + expect((service as ProtobufInternal).pendingCommands.size).toBe(1); }); }); @@ -368,25 +395,25 @@ describe('ProtobufService', () => { it('returns early when event is undefined', () => { const service = new ProtobufService(mockSocket); vi.mocked(hasExtension).mockReturnValue(false); - (service as any).processRoomEvent(undefined); + (service as ProtobufInternal).processRoomEvent(undefined); expect(hasExtension).not.toHaveBeenCalled(); }); it('dispatches to a RoomEvents handler when hasExtension returns true', () => { const service = new ProtobufService(mockSocket); const handler = vi.fn(); - const mockExt = {}; + const mockExt = {} as GenExtension; const payload = { roomData: 1 }; - (RoomEvents as any).push([mockExt, handler]); + (RoomEvents as RoomExtensionRegistry).push([mockExt, handler]); vi.mocked(hasExtension).mockReturnValue(true); vi.mocked(getExtension).mockReturnValue(payload); const event = { roomId: 10 }; - (service as any).processRoomEvent(event); + (service as ProtobufInternal).processRoomEvent(event); expect(handler).toHaveBeenCalledWith(payload, event); - (RoomEvents as any).pop(); + (RoomEvents as RoomExtensionRegistry).pop(); }); }); @@ -394,24 +421,24 @@ describe('ProtobufService', () => { it('returns early when event is undefined', () => { const service = new ProtobufService(mockSocket); vi.mocked(hasExtension).mockReturnValue(false); - (service as any).processSessionEvent(undefined); + (service as ProtobufInternal).processSessionEvent(undefined); expect(hasExtension).not.toHaveBeenCalled(); }); it('dispatches to a SessionEvents handler when hasExtension returns true', () => { const service = new ProtobufService(mockSocket); const handler = vi.fn(); - const mockExt = {}; + const mockExt = {} as GenExtension; const payload = { sessionData: 1 }; - (SessionEvents as any).push([mockExt, handler]); + (SessionEvents as SessionExtensionRegistry).push([mockExt, handler]); vi.mocked(hasExtension).mockReturnValue(true); vi.mocked(getExtension).mockReturnValue(payload); - (service as any).processSessionEvent({ sessionId: 7 }); + (service as ProtobufInternal).processSessionEvent({ sessionId: 7 }); expect(handler).toHaveBeenCalledWith(payload); - (SessionEvents as any).pop(); + (SessionEvents as SessionExtensionRegistry).pop(); }); }); diff --git a/webclient/src/websocket/services/ProtobufService.ts b/webclient/src/websocket/services/ProtobufService.ts index 7333c58b2..4bc148ece 100644 --- a/webclient/src/websocket/services/ProtobufService.ts +++ b/webclient/src/websocket/services/ProtobufService.ts @@ -1,21 +1,10 @@ import { create, fromBinary, hasExtension, getExtension, setExtension, toBinary } from '@bufbuild/protobuf'; import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; -import type { Response } from 'generated/proto/response_pb'; -import type { RoomEvent } from 'generated/proto/room_event_pb'; -import type { SessionEvent } from 'generated/proto/session_event_pb'; -import type { GameEventContainer } from 'generated/proto/game_event_container_pb'; import { GameEvents, RoomEvents, SessionEvents } from '../events'; -import { GameEventMeta } from 'types'; +import { Data, Enriched } from '@app/types'; -import { CommandContainerSchema, type CommandContainer } from 'generated/proto/commands_pb'; -import { ServerMessageSchema, ServerMessage_MessageType, type ServerMessage } from 'generated/proto/server_message_pb'; -import { SessionCommandSchema, type SessionCommand } from 'generated/proto/session_commands_pb'; -import { GameCommandSchema, type GameCommand } from 'generated/proto/game_commands_pb'; -import { RoomCommandSchema, type RoomCommand } from 'generated/proto/room_commands_pb'; -import { ModeratorCommandSchema, type ModeratorCommand } from 'generated/proto/moderator_commands_pb'; -import { AdminCommandSchema, type AdminCommand } from 'generated/proto/admin_commands_pb'; import { type CommandOptions, handleResponse } from './command-options'; @@ -26,7 +15,7 @@ export interface SocketTransport { export class ProtobufService { private cmdId = 0; - private pendingCommands = new Map void>(); + private pendingCommands = new Map void>(); private transport: SocketTransport; @@ -41,13 +30,13 @@ export class ProtobufService { public sendGameCommand( gameId: number, - ext: GenExtension, + ext: GenExtension, value: V, options?: CommandOptions ): void { - const gameCmd = create(GameCommandSchema); + const gameCmd = create(Data.GameCommandSchema); setExtension(gameCmd, ext, value); - const cmd = create(CommandContainerSchema, { gameId, gameCommand: [gameCmd] }); + const cmd = create(Data.CommandContainerSchema, { gameId, gameCommand: [gameCmd] }); this.sendCommand(cmd, raw => { if (options) { handleResponse(ext.typeName, raw, options); @@ -57,13 +46,13 @@ export class ProtobufService { public sendRoomCommand( roomId: number, - ext: GenExtension, + ext: GenExtension, value: V, options?: CommandOptions ): void { - const roomCmd = create(RoomCommandSchema); + const roomCmd = create(Data.RoomCommandSchema); setExtension(roomCmd, ext, value); - const cmd = create(CommandContainerSchema, { roomId, roomCommand: [roomCmd] }); + const cmd = create(Data.CommandContainerSchema, { roomId, roomCommand: [roomCmd] }); this.sendCommand(cmd, raw => { if (options) { handleResponse(ext.typeName, raw, options); @@ -72,13 +61,13 @@ export class ProtobufService { } public sendSessionCommand( - ext: GenExtension, + ext: GenExtension, value: V, options?: CommandOptions ): void { - const sesCmd = create(SessionCommandSchema); + const sesCmd = create(Data.SessionCommandSchema); setExtension(sesCmd, ext, value); - const cmd = create(CommandContainerSchema, { sessionCommand: [sesCmd] }); + const cmd = create(Data.CommandContainerSchema, { sessionCommand: [sesCmd] }); this.sendCommand(cmd, raw => { if (options) { handleResponse(ext.typeName, raw, options); @@ -87,13 +76,13 @@ export class ProtobufService { } public sendModeratorCommand( - ext: GenExtension, + ext: GenExtension, value: V, options?: CommandOptions ): void { - const modCmd = create(ModeratorCommandSchema); + const modCmd = create(Data.ModeratorCommandSchema); setExtension(modCmd, ext, value); - const cmd = create(CommandContainerSchema, { moderatorCommand: [modCmd] }); + const cmd = create(Data.CommandContainerSchema, { moderatorCommand: [modCmd] }); this.sendCommand(cmd, raw => { if (options) { handleResponse(ext.typeName, raw, options); @@ -102,13 +91,13 @@ export class ProtobufService { } public sendAdminCommand( - ext: GenExtension, + ext: GenExtension, value: V, options?: CommandOptions ): void { - const adminCmd = create(AdminCommandSchema); + const adminCmd = create(Data.AdminCommandSchema); setExtension(adminCmd, ext, value); - const cmd = create(CommandContainerSchema, { adminCommand: [adminCmd] }); + const cmd = create(Data.CommandContainerSchema, { adminCommand: [adminCmd] }); this.sendCommand(cmd, raw => { if (options) { handleResponse(ext.typeName, raw, options); @@ -116,34 +105,34 @@ export class ProtobufService { }); } - public sendCommand(cmd: CommandContainer, callback: (raw: Response) => void) { + public sendCommand(cmd: Data.CommandContainer, callback: (raw: Data.Response) => void) { this.cmdId++; cmd.cmdId = BigInt(this.cmdId); this.pendingCommands.set(this.cmdId, callback); if (this.transport.isOpen()) { - this.transport.send(toBinary(CommandContainerSchema, cmd)); + this.transport.send(toBinary(Data.CommandContainerSchema, cmd)); } } public handleMessageEvent({ data }: MessageEvent): void { try { const uint8msg = new Uint8Array(data); - const msg: ServerMessage = fromBinary(ServerMessageSchema, uint8msg); + const msg: Data.ServerMessage = fromBinary(Data.ServerMessageSchema, uint8msg); if (msg) { switch (msg.messageType) { - case ServerMessage_MessageType.RESPONSE: + case Data.ServerMessage_MessageType.RESPONSE: this.processServerResponse(msg.response); break; - case ServerMessage_MessageType.ROOM_EVENT: + case Data.ServerMessage_MessageType.ROOM_EVENT: this.processRoomEvent(msg.roomEvent); break; - case ServerMessage_MessageType.SESSION_EVENT: + case Data.ServerMessage_MessageType.SESSION_EVENT: this.processSessionEvent(msg.sessionEvent); break; - case ServerMessage_MessageType.GAME_EVENT_CONTAINER: + case Data.ServerMessage_MessageType.GAME_EVENT_CONTAINER: this.processGameEvent(msg.gameEventContainer); break; default: @@ -156,7 +145,7 @@ export class ProtobufService { } } - private processServerResponse(response: Response | undefined) { + private processServerResponse(response: Data.Response | undefined) { if (!response) { return; } @@ -168,7 +157,7 @@ export class ProtobufService { } } - private processRoomEvent(event: RoomEvent | undefined) { + private processRoomEvent(event: Data.RoomEvent | undefined) { if (!event) { return; } @@ -180,7 +169,7 @@ export class ProtobufService { } } - private processSessionEvent(event: SessionEvent | undefined) { + private processSessionEvent(event: Data.SessionEvent | undefined) { if (!event) { return; } @@ -192,7 +181,7 @@ export class ProtobufService { } } - private processGameEvent(container: GameEventContainer | undefined): void { + private processGameEvent(container: Data.GameEventContainer | undefined): void { if (!container?.eventList?.length) { return; } @@ -200,7 +189,7 @@ export class ProtobufService { const { gameId, context, secondsElapsed, forcedByJudge } = container; for (const event of container.eventList) { - const meta: GameEventMeta = { + const meta: Enriched.GameEventMeta = { gameId: gameId ?? -1, playerId: event.playerId ?? -1, context, diff --git a/webclient/src/websocket/services/WebSocketService.spec.ts b/webclient/src/websocket/services/WebSocketService.spec.ts index f01c2b2c1..a806b512f 100644 --- a/webclient/src/websocket/services/WebSocketService.spec.ts +++ b/webclient/src/websocket/services/WebSocketService.spec.ts @@ -22,18 +22,24 @@ vi.mock('../persistence', () => ({ })); import { WebSocketService } from './WebSocketService'; +import type { WebSocketServiceConfig } from './WebSocketService'; +import { KeepAliveService } from './KeepAliveService'; import { SessionPersistence } from '../persistence'; import { updateStatus } from '../commands/session'; -import { StatusEnum } from 'types'; +import { App } from '@app/types'; + +type WebSocketInternal = WebSocketService & { + keepAliveService: KeepAliveService; + testSocket: WebSocket | null; +}; let MockWS: Mock; let mockInstance: ReturnType['mockInstance']; let restoreWebSocket: ReturnType['restore']; -let mockConfig: any; +let mockConfig: WebSocketServiceConfig; beforeEach(() => { vi.useFakeTimers(); - vi.clearAllMocks(); const installed = installMockWebSocket(); MockWS = installed.MockWS; @@ -53,13 +59,13 @@ afterEach(() => { describe('WebSocketService', () => { function createConnectedService() { const service = new WebSocketService(mockConfig); - service.connect({ host: 'h', port: 1 } as any, 'ws'); + service.connect({ host: 'h', port: '1' }, 'ws'); return service; } function createTestConnectedService() { const service = new WebSocketService(mockConfig); - service.testConnect({ host: 'h', port: 1 } as any, 'ws'); + service.testConnect({ host: 'h', port: '1' }, 'ws'); return service; } @@ -71,11 +77,11 @@ describe('WebSocketService', () => { it('calls disconnect and updateStatus when keepAlive disconnected$ fires', () => { const service = new WebSocketService(mockConfig); - service.connect({ host: 'localhost', port: 8080 } as any, 'ws'); + service.connect({ host: 'localhost', port: '8080' }, 'ws'); // trigger keepAliveService.disconnected$ - (service as any).keepAliveService.disconnected$.next(); + (service as WebSocketInternal).keepAliveService.disconnected$.next(); expect(mockInstance.close).toHaveBeenCalled(); - expect(updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'Connection timeout'); + expect(updateStatus).toHaveBeenCalledWith(App.StatusEnum.DISCONNECTED, 'Connection timeout'); }); }); @@ -87,7 +93,7 @@ describe('WebSocketService', () => { writable: true, configurable: true, }); - service.connect({ host: 'example.com', port: 8080 } as any); + service.connect({ host: 'example.com', port: '8080' }); expect(MockWS).toHaveBeenCalledWith('wss://example.com:8080'); }); @@ -98,7 +104,7 @@ describe('WebSocketService', () => { writable: true, configurable: true, }); - service.connect({ host: 'somehost', port: 1234 } as any); + service.connect({ host: 'somehost', port: '1234' }); expect(MockWS).toHaveBeenCalledWith('ws://somehost:1234'); }); @@ -125,21 +131,21 @@ describe('WebSocketService', () => { it('calls updateStatus CONNECTED on open', () => { createConnectedService(); mockInstance.onopen(); - expect(updateStatus).toHaveBeenCalledWith(StatusEnum.CONNECTED, 'Connected'); + expect(updateStatus).toHaveBeenCalledWith(App.StatusEnum.CONNECTED, 'Connected'); }); it('starts the ping loop with the keepalive interval', () => { const service = new WebSocketService(mockConfig); - const startSpy = vi.spyOn((service as any).keepAliveService, 'startPingLoop'); - service.connect({ host: 'h', port: 1 } as any, 'ws'); + const startSpy = vi.spyOn((service as WebSocketInternal).keepAliveService, 'startPingLoop'); + service.connect({ host: 'h', port: '1' }, 'ws'); mockInstance.onopen(); expect(startSpy).toHaveBeenCalledWith(1000, expect.any(Function)); }); it('ping loop callback calls keepAliveFn', () => { const service = new WebSocketService(mockConfig); - const startSpy = vi.spyOn((service as any).keepAliveService, 'startPingLoop'); - service.connect({ host: 'h', port: 1 } as any, 'ws'); + const startSpy = vi.spyOn((service as WebSocketInternal).keepAliveService, 'startPingLoop'); + service.connect({ host: 'h', port: '1' }, 'ws'); mockInstance.onopen(); const pingCb = startSpy.mock.calls[0][1] as (done: Function) => void; const done = vi.fn(); @@ -152,20 +158,20 @@ describe('WebSocketService', () => { it('calls updateStatus DISCONNECTED on close when not already DISCONNECTED', () => { createConnectedService(); mockInstance.onclose(); - expect(updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'Connection Closed'); + expect(updateStatus).toHaveBeenCalledWith(App.StatusEnum.DISCONNECTED, 'Connection Closed'); }); it('does not overwrite status if already DISCONNECTED', () => { createConnectedService(); mockInstance.onerror(); mockInstance.onclose(); - expect(updateStatus).not.toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'Connection Closed'); + expect(updateStatus).not.toHaveBeenCalledWith(App.StatusEnum.DISCONNECTED, 'Connection Closed'); }); it('ends the ping loop on close', () => { const service = new WebSocketService(mockConfig); - const endSpy = vi.spyOn((service as any).keepAliveService, 'endPingLoop'); - service.connect({ host: 'h', port: 1 } as any, 'ws'); + const endSpy = vi.spyOn((service as WebSocketInternal).keepAliveService, 'endPingLoop'); + service.connect({ host: 'h', port: '1' }, 'ws'); mockInstance.onclose(); expect(endSpy).toHaveBeenCalled(); }); @@ -175,7 +181,7 @@ describe('WebSocketService', () => { it('calls updateStatus DISCONNECTED on error', () => { createConnectedService(); mockInstance.onerror(); - expect(updateStatus).toHaveBeenCalledWith(StatusEnum.DISCONNECTED, 'Connection Failed'); + expect(updateStatus).toHaveBeenCalledWith(App.StatusEnum.DISCONNECTED, 'Connection Failed'); }); it('calls SessionPersistence.connectionFailed on error', () => { @@ -242,7 +248,7 @@ describe('WebSocketService', () => { writable: true, configurable: true, }); - service.testConnect({ host: 'example.com', port: 9000 } as any); + service.testConnect({ host: 'example.com', port: '9000' }); expect(MockWS).toHaveBeenCalledWith('wss://example.com:9000'); }); @@ -253,17 +259,17 @@ describe('WebSocketService', () => { writable: true, configurable: true, }); - service.testConnect({ host: 'h', port: 1 } as any); + service.testConnect({ host: 'h', port: '1' }); expect(MockWS).toHaveBeenCalledWith('ws://h:1'); }); it('closes previous testSocket when connecting again', () => { const service = new WebSocketService(mockConfig); - service.testConnect({ host: 'h', port: 1 } as any, 'ws'); + service.testConnect({ host: 'h', port: '1' }, 'ws'); const firstInstance = mockInstance; // install second mock instance and restore after test const installed2 = installMockWebSocket(); - service.testConnect({ host: 'h', port: 2 } as any, 'ws'); + service.testConnect({ host: 'h', port: '2' }, 'ws'); expect(firstInstance.close).toHaveBeenCalled(); // restore original mock so subsequent tests see a clean global mockInstance = installed2.mockInstance; @@ -293,7 +299,7 @@ describe('WebSocketService', () => { it('nulls out testSocket on close', () => { const service = createTestConnectedService(); mockInstance.onclose(); - expect((service as any).testSocket).toBeNull(); + expect((service as WebSocketInternal).testSocket).toBeNull(); }); }); }); diff --git a/webclient/src/websocket/services/WebSocketService.ts b/webclient/src/websocket/services/WebSocketService.ts index d0b09eab5..0ca13b7e6 100644 --- a/webclient/src/websocket/services/WebSocketService.ts +++ b/webclient/src/websocket/services/WebSocketService.ts @@ -1,6 +1,6 @@ import { Subject } from 'rxjs'; -import { StatusEnum, WebSocketConnectOptions } from 'types'; +import { App, Enriched } from '@app/types'; import { KeepAliveService } from './KeepAliveService'; import { CLIENT_OPTIONS } from '../config'; @@ -29,11 +29,11 @@ export class WebSocketService { this.keepAliveService = new KeepAliveService(this); this.keepAliveService.disconnected$.subscribe(() => { this.disconnect(); - updateStatus(StatusEnum.DISCONNECTED, 'Connection timeout'); + updateStatus(App.StatusEnum.DISCONNECTED, 'Connection timeout'); }); } - public connect(options: WebSocketConnectOptions, protocol: string = 'wss'): void { + public connect(options: Enriched.WebSocketConnectOptions, protocol: string = 'wss'): void { if (window.location.hostname === 'localhost') { protocol = 'ws'; } @@ -44,7 +44,7 @@ export class WebSocketService { this.socket = this.createWebSocket(`${protocol}://${host}:${port}`); } - public testConnect(options: WebSocketConnectOptions, protocol: string = 'wss'): void { + public testConnect(options: Enriched.WebSocketConnectOptions, protocol: string = 'wss'): void { if (window.location.hostname === 'localhost') { protocol = 'ws'; } @@ -77,7 +77,7 @@ export class WebSocketService { socket.onopen = () => { clearTimeout(connectionTimer); this.errorFired = false; - updateStatus(StatusEnum.CONNECTED, 'Connected'); + updateStatus(App.StatusEnum.CONNECTED, 'Connected'); this.keepAliveService.startPingLoop(this.keepalive, (pingReceived: () => void) => { this.config.keepAliveFn(pingReceived); @@ -87,7 +87,7 @@ export class WebSocketService { socket.onclose = () => { // dont overwrite failure messages if (!this.errorFired) { - updateStatus(StatusEnum.DISCONNECTED, 'Connection Closed'); + updateStatus(App.StatusEnum.DISCONNECTED, 'Connection Closed'); } this.errorFired = false; this.keepAliveService.endPingLoop(); @@ -95,7 +95,7 @@ export class WebSocketService { socket.onerror = () => { this.errorFired = true; - updateStatus(StatusEnum.DISCONNECTED, 'Connection Failed'); + updateStatus(App.StatusEnum.DISCONNECTED, 'Connection Failed'); SessionPersistence.connectionFailed(); }; diff --git a/webclient/src/websocket/services/command-options.spec.ts b/webclient/src/websocket/services/command-options.spec.ts index 8c520781c..8548c6c06 100644 --- a/webclient/src/websocket/services/command-options.spec.ts +++ b/webclient/src/websocket/services/command-options.spec.ts @@ -1,12 +1,12 @@ -vi.mock('@bufbuild/protobuf', () => ({ - getExtension: vi.fn(), -})); +import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; +import { Data } from '@app/types'; +vi.mock('@bufbuild/protobuf', async () => { + const actual = await vi.importActual('@bufbuild/protobuf'); + return { ...actual, getExtension: vi.fn() }; +}); -vi.mock('generated/proto/response_pb', () => ({ - Response_ResponseCode: { RespOk: 1 }, -})); +import { create, getExtension } from '@bufbuild/protobuf'; -import { getExtension } from '@bufbuild/protobuf'; import { handleResponse } from './command-options'; beforeEach(() => { @@ -17,42 +17,43 @@ describe('handleResponse', () => { it('calls onResponse and returns early when provided', () => { const onResponse = vi.fn(); const onSuccess = vi.fn(); - handleResponse('test', { responseCode: 99 } as any, { onResponse, onSuccess }); + handleResponse('test', create(Data.ResponseSchema, { responseCode: 99 }), { onResponse, onSuccess }); expect(onResponse).toHaveBeenCalled(); expect(onSuccess).not.toHaveBeenCalled(); }); it('calls onSuccess when responseCode is RespOk and no responseExt', () => { const onSuccess = vi.fn(); - const raw = { responseCode: 1 } as any; + const raw = create(Data.ResponseSchema, { responseCode: Data.Response_ResponseCode.RespOk }); handleResponse('test', raw, { onSuccess }); expect(onSuccess).toHaveBeenCalledWith(); }); it('calls onSuccess with nested response when responseExt is set', () => { - vi.mocked(getExtension).mockReturnValue({ nested: true } as any); + vi.mocked(getExtension).mockReturnValue({ nested: true }); const onSuccess = vi.fn(); - const fakeExt = {} as any; - const raw = { responseCode: 1 } as any; + const fakeExt = {} as unknown as GenExtension; + const raw = create(Data.ResponseSchema, { responseCode: Data.Response_ResponseCode.RespOk }); handleResponse('test', raw, { onSuccess, responseExt: fakeExt }); expect(onSuccess).toHaveBeenCalledWith({ nested: true }, raw); }); it('calls onResponseCode handler when code matches', () => { const specificHandler = vi.fn(); - handleResponse('test', { responseCode: 5 } as any, { onResponseCode: { 5: specificHandler } }); + handleResponse('test', create(Data.ResponseSchema, { responseCode: 5 }), { onResponseCode: { 5: specificHandler } }); expect(specificHandler).toHaveBeenCalled(); }); it('calls onError when responseCode is not RespOk and no specific handler', () => { const onError = vi.fn(); - handleResponse('test', { responseCode: 99 } as any, { onError }); - expect(onError).toHaveBeenCalledWith(99, { responseCode: 99 }); + const raw = create(Data.ResponseSchema, { responseCode: 99 }); + handleResponse('test', raw, { onError }); + expect(onError).toHaveBeenCalledWith(99, raw); }); it('logs error to console when no callbacks for non-RespOk response', () => { const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {}); - handleResponse('test.Type', { responseCode: 42 } as any, {}); + handleResponse('test.Type', create(Data.ResponseSchema, { responseCode: 42 }), {}); expect(consoleSpy).toHaveBeenCalled(); consoleSpy.mockRestore(); }); diff --git a/webclient/src/websocket/services/command-options.ts b/webclient/src/websocket/services/command-options.ts index d9d3b0be8..671d0a02b 100644 --- a/webclient/src/websocket/services/command-options.ts +++ b/webclient/src/websocket/services/command-options.ts @@ -1,16 +1,16 @@ import { getExtension } from '@bufbuild/protobuf'; import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; -import { Response_ResponseCode, type Response } from 'generated/proto/response_pb'; +import { Data } from '@app/types'; interface CommandOptionsBase { - onError?: (responseCode: number, raw: Response) => void; - onResponseCode?: { [code: number]: (raw: Response) => void }; - onResponse?: (raw: Response) => void; + onError?: (responseCode: number, raw: Data.Response) => void; + onResponseCode?: { [code: number]: (raw: Data.Response) => void }; + onResponse?: (raw: Data.Response) => void; } export interface CommandOptionsWithResponse extends CommandOptionsBase { - responseExt: GenExtension; - onSuccess?: (response: R, raw: Response) => void; + responseExt: GenExtension; + onSuccess?: (response: R, raw: Data.Response) => void; } export interface CommandOptionsWithoutResponse extends CommandOptionsBase { @@ -24,7 +24,7 @@ export function hasResponseExt(options: CommandOptions): options is Comman return options.responseExt !== undefined; } -export function handleResponse(typeName: string, raw: Response, options: CommandOptions): void { +export function handleResponse(typeName: string, raw: Data.Response, options: CommandOptions): void { if (options.onResponse) { options.onResponse(raw); return; @@ -32,7 +32,7 @@ export function handleResponse(typeName: string, raw: Response, options: Comm const { responseCode } = raw; - if (responseCode === Response_ResponseCode.RespOk) { + if (responseCode === Data.Response_ResponseCode.RespOk) { if (hasResponseExt(options)) { options.onSuccess?.(getExtension(raw, options.responseExt), raw); } else { diff --git a/webclient/src/websocket/services/protobuf-types.ts b/webclient/src/websocket/services/protobuf-types.ts deleted file mode 100644 index 395442de7..000000000 --- a/webclient/src/websocket/services/protobuf-types.ts +++ /dev/null @@ -1,44 +0,0 @@ -import type { GenExtension } from '@bufbuild/protobuf/codegenv2'; -import type { RoomEvent } from 'generated/proto/room_event_pb'; -import type { SessionEvent } from 'generated/proto/session_event_pb'; -import type { GameEvent } from 'generated/proto/game_event_pb'; -import type { GameEventMeta } from 'types'; - -type SessionRegistryEntry = [ - GenExtension, - (value: V) => void -]; -export type SessionExtensionRegistry = SessionRegistryEntry[]; - -type RoomRegistryEntry = [ - GenExtension, - (value: V, roomEvent: RoomEvent) => void -]; -export type RoomExtensionRegistry = RoomRegistryEntry[]; - -type GameRegistryEntry = [ - GenExtension, - (value: V, meta: GameEventMeta) => void -]; -export type GameExtensionRegistry = GameRegistryEntry[]; - -export function makeSessionEntry( - ext: GenExtension, - handler: (value: V) => void -): SessionRegistryEntry { - return [ext as GenExtension, handler as (value: unknown) => void]; -} - -export function makeRoomEntry( - ext: GenExtension, - handler: (value: V, roomEvent: RoomEvent) => void -): RoomRegistryEntry { - return [ext as GenExtension, handler as RoomRegistryEntry[1]]; -} - -export function makeGameEntry( - ext: GenExtension, - handler: (value: V, meta: GameEventMeta) => void -): GameRegistryEntry { - return [ext as GenExtension, handler as GameRegistryEntry[1]]; -} diff --git a/webclient/src/websocket/utils/passwordHasher.spec.ts b/webclient/src/websocket/utils/passwordHasher.spec.ts index 7ab16d128..388261b36 100644 --- a/webclient/src/websocket/utils/passwordHasher.spec.ts +++ b/webclient/src/websocket/utils/passwordHasher.spec.ts @@ -1,4 +1,4 @@ -vi.mock('generated/proto/event_server_identification_pb', () => ({ +vi.mock('../../generated/proto/event_server_identification_pb', () => ({ Event_ServerIdentification_ServerOptions: { SupportsPasswordHash: 2 }, })); diff --git a/webclient/src/websocket/utils/passwordHasher.ts b/webclient/src/websocket/utils/passwordHasher.ts index 3f726f10f..22951ce49 100644 --- a/webclient/src/websocket/utils/passwordHasher.ts +++ b/webclient/src/websocket/utils/passwordHasher.ts @@ -1,6 +1,6 @@ import sha512 from 'crypto-js/sha512'; import Base64 from 'crypto-js/enc-base64'; -import { Event_ServerIdentification_ServerOptions } from 'generated/proto/event_server_identification_pb'; +import { Data } from '@app/types'; const HASH_ROUNDS = 1_000; const SALT_LENGTH = 16; @@ -28,5 +28,5 @@ export const generateSalt = (): string => { export const passwordSaltSupported = (serverOptions: number): number => { // Intentional use of Bitwise operator b/c of how Servatrice Enums work - return serverOptions & Event_ServerIdentification_ServerOptions.SupportsPasswordHash; + return serverOptions & Data.Event_ServerIdentification_ServerOptions.SupportsPasswordHash; } diff --git a/webclient/tsconfig.json b/webclient/tsconfig.json index 3ce50dc96..1e851244e 100644 --- a/webclient/tsconfig.json +++ b/webclient/tsconfig.json @@ -1,6 +1,5 @@ { "compilerOptions": { - "baseUrl": "src", "target": "es2020", "lib": [ "dom", @@ -10,6 +9,7 @@ "typeRoots": [ "node_modules/@types" ], + "types": ["node"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, @@ -22,7 +22,21 @@ "isolatedModules": true, "noEmit": true, "jsx": "react-jsx", - "noFallthroughCasesInSwitch": false + "noFallthroughCasesInSwitch": false, + "paths": { + "@app/api": ["./src/api/index.ts"], + "@app/components": ["./src/components/index.ts"], + "@app/containers": ["./src/containers/index.ts"], + "@app/dialogs": ["./src/dialogs/index.ts"], + "@app/forms": ["./src/forms/index.ts"], + "@app/hooks": ["./src/hooks/index.ts"], + "@app/images": ["./src/images/index.ts"], + "@app/services": ["./src/services/index.ts"], + "@app/store": ["./src/store/index.ts"], + "@app/types": ["./src/types/index.ts"], + "@app/websocket": ["./src/websocket/index.ts"], + "@app/generated": ["./src/generated/index.ts"] + } }, "include": [ "src"