From f32994fbc786407355e3e4be64d623e0667da6a0 Mon Sep 17 00:00:00 2001 From: Ryan <7389646+ryanbarlow97@users.noreply.github.com> Date: Wed, 15 Apr 2026 23:14:11 +0100 Subject: [PATCH] Account Modal Bugfix (#3687) ## Description: Fix null stat values from LEFT JOIN causing Zod validation failure on player profiles https://github.com/openfrontio/infra/pull/316 switched playerStats from innerJoin to leftJoin so that sessions with no stats row (games that ended instantly on spawn) are still counted in wins/losses/total. ## 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 ## Please put your Discord username so you can be contacted if a bug or regression is found: w.o.n --- src/core/StatsSchemas.ts | 1 + tests/StatsSchema.test.ts | 29 +++++++++++++++++++++++++++++ 2 files changed, 30 insertions(+) diff --git a/src/core/StatsSchemas.ts b/src/core/StatsSchemas.ts index c37607bd6..8543f4bab 100644 --- a/src/core/StatsSchemas.ts +++ b/src/core/StatsSchemas.ts @@ -94,6 +94,7 @@ export const OTHER_INDEX_LOST = 3; // Structures/warships destroyed/captured by export const OTHER_INDEX_UPGRADE = 4; // Structures upgraded export const BigIntStringSchema = z.preprocess((val) => { + if (val === null) return 0n; if (typeof val === "string" && /^-?\d+$/.test(val)) return BigInt(val); if (typeof val === "bigint") return val; return val; diff --git a/tests/StatsSchema.test.ts b/tests/StatsSchema.test.ts index 8b8f6b5dd..b513e9537 100644 --- a/tests/StatsSchema.test.ts +++ b/tests/StatsSchema.test.ts @@ -1,3 +1,4 @@ +import { PlayerStatsLeafSchema } from "../src/core/ApiSchemas"; import { PlayerStatsSchema } from "../src/core/StatsSchemas"; function testPlayerSchema( @@ -45,4 +46,32 @@ describe("StatsSchema", () => { testPlayerSchema("{", false, true); testPlayerSchema("{}}", false, true); }); + + test("null array elements coerce to 0n (LEFT JOIN rows with no stats)", () => { + // Postgres SUM() over all-NULL rows returns NULL. These should parse as 0n. + testPlayerSchema( + '{"attacks":[null,null,null],"betrayals":null,"gold":[null,null,null,null,null,null]}', + ); + }); +}); + +describe("PlayerStatsLeafSchema", () => { + test("null stat values coerce to 0n", () => { + const result = PlayerStatsLeafSchema.safeParse({ + wins: "0", + losses: "1", + total: "1", + stats: { attacks: [null, null, null], betrayals: null }, + }); + expect(result.success).toBe(true); + }); + + test("missing required field (wins) still fails — undefined is not coerced", () => { + const result = PlayerStatsLeafSchema.safeParse({ + losses: "1", + total: "1", + stats: {}, + }); + expect(result.success).toBe(false); + }); });