From 13f0c2303e958f4f48b3d37fbdad40ed20106df0 Mon Sep 17 00:00:00 2001 From: Scott Anderson <662325+scottanderson@users.noreply.github.com> Date: Tue, 24 Jun 2025 16:50:36 -0400 Subject: [PATCH] bug: StatsSchema zod validation error (#1267) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Description: Fix a bug in the StatsSchema zod validation logic. In zod v4, the `record` function has been renamed to `partialRecord`, and the new `record` function requires that all keys are present. We intentionally omit empty stats, so this causes a zod validation error. Example error: ``` Connection error! game id: VSEtmKpJ, client id: 0UMrA84F Error: ✖ Invalid input: expected array, received undefined → at allPlayersStats.0UMrA84F.units.wshp ✖ Invalid input: expected array, received undefined → at allPlayersStats.0UMrA84F.units.saml Stack: ``` ## Please complete the following: - [x] I have added screenshots for all UI updates - [x] I process any text displayed to the user through translateText() and I've added it to the en.json file - [x] I have added relevant tests to the test directory - [x] I confirm I have thoroughly tested these changes and take full responsibility for any bugs introduced - [x] I understand that submitting code with bugs that could have been caught through manual testing blocks releases and new features for all contributors --- src/core/StatsSchemas.ts | 6 ++--- tests/StatsSchema.test.ts | 48 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 tests/StatsSchema.test.ts diff --git a/src/core/StatsSchemas.ts b/src/core/StatsSchemas.ts index e6e6fa519..03d151ad2 100644 --- a/src/core/StatsSchemas.ts +++ b/src/core/StatsSchemas.ts @@ -98,10 +98,10 @@ export const PlayerStatsSchema = z .object({ attacks: AtLeastOneNumberSchema.optional(), betrayals: BigIntStringSchema.optional(), - boats: z.record(BoatUnitSchema, AtLeastOneNumberSchema).optional(), - bombs: z.record(BombUnitSchema, AtLeastOneNumberSchema).optional(), + boats: z.partialRecord(BoatUnitSchema, AtLeastOneNumberSchema).optional(), + bombs: z.partialRecord(BombUnitSchema, AtLeastOneNumberSchema).optional(), gold: AtLeastOneNumberSchema.optional(), - units: z.record(OtherUnitSchema, AtLeastOneNumberSchema).optional(), + units: z.partialRecord(OtherUnitSchema, AtLeastOneNumberSchema).optional(), }) .optional(); export type PlayerStats = z.infer; diff --git a/tests/StatsSchema.test.ts b/tests/StatsSchema.test.ts new file mode 100644 index 000000000..8b8f6b5dd --- /dev/null +++ b/tests/StatsSchema.test.ts @@ -0,0 +1,48 @@ +import { PlayerStatsSchema } from "../src/core/StatsSchemas"; + +function testPlayerSchema( + json: string, + expectSuccess = true, + expectThrow = false, +): void { + const parse = () => { + const raw = JSON.parse(json); + const result = PlayerStatsSchema.safeParse(raw); + return result.success; + }; + + if (expectSuccess) { + // Expect success + expect(parse()).toBeTruthy(); + } else if (!expectThrow) { + // Expect failure + expect(parse()).toBeFalsy(); + } else { + // Expect throw + expect(parse).toThrow(); + } +} + +describe("StatsSchema", () => { + test("Parse empty", () => { + testPlayerSchema("{}"); + }); + + test("Parse partial", () => { + testPlayerSchema('{"units":{"port":["0","0","0","1"]}}'); + }); + + test("Parse invalid", () => { + testPlayerSchema("[]", false); + testPlayerSchema("null", false); + testPlayerSchema('"null"', false); + testPlayerSchema('"undefined"', false); + }); + + test("Parse failure", () => { + testPlayerSchema("", false, true); + testPlayerSchema("undefined", false, true); + testPlayerSchema("{", false, true); + testPlayerSchema("{}}", false, true); + }); +});