diff --git a/src/core/ApiSchemas.ts b/src/core/ApiSchemas.ts index 0b74f86c0..219bb910a 100644 --- a/src/core/ApiSchemas.ts +++ b/src/core/ApiSchemas.ts @@ -246,7 +246,7 @@ export const PublicPlayerGameSchema = z.object({ gameId: z.string(), start: z.iso.datetime(), durationSeconds: z.number().int().nonnegative(), - map: z.string(), + map: z.string().trim(), mode: z.string(), type: z.string(), playerTeams: z.string().nullable(), diff --git a/src/core/ClanApiSchemas.ts b/src/core/ClanApiSchemas.ts index 0246e94d0..99da2b46c 100644 --- a/src/core/ClanApiSchemas.ts +++ b/src/core/ClanApiSchemas.ts @@ -198,7 +198,7 @@ export const ClanGameSchema = z.object({ gameId: z.string(), start: z.iso.datetime(), durationSeconds: z.number().int().nonnegative(), - map: z.string().optional(), + map: z.string().trim().optional(), mode: z.string().optional(), // playerTeams is `null` (not absent) for FFA / non-team games — use // `.nullish()` so the wire `null` doesn't fail the parse. diff --git a/src/core/Schemas.ts b/src/core/Schemas.ts index 8daecd410..0bbb7e7b9 100644 --- a/src/core/Schemas.ts +++ b/src/core/Schemas.ts @@ -947,6 +947,10 @@ const ArchivedPlayerRecordSchema = PlayerRecordSchema.extend({ export const ArchivedAnalyticsRecordSchema = AnalyticsRecordSchema.extend({ info: GameEndInfoSchema.extend({ config: GameConfigSchema.extend({ + gameMap: z.preprocess( + (value) => (typeof value === "string" ? value.trim() : value), + GameConfigSchema.shape.gameMap, + ), // predates configurable nation count nations: GameConfigSchema.shape.nations .catch("default") diff --git a/tests/ApiSchemas.test.ts b/tests/ApiSchemas.test.ts index 8be21e42a..aa2d115e2 100644 --- a/tests/ApiSchemas.test.ts +++ b/tests/ApiSchemas.test.ts @@ -119,6 +119,17 @@ describe("PublicPlayerGameSchema", () => { expect(PublicPlayerGameSchema.safeParse(validGame).success).toBe(true); }); + it("normalizes accidental whitespace around archived map names", () => { + const result = PublicPlayerGameSchema.safeParse({ + ...validGame, + map: "Deglaciated Antarctica ", + }); + + expect(result.success).toBe(true); + if (!result.success) return; + expect(result.data.map).toBe("Deglaciated Antarctica"); + }); + it("accepts clanTag: null (not repping a clan)", () => { expect( PublicPlayerGameSchema.safeParse({ ...validGame, clanTag: null }).success, diff --git a/tests/ArchivedRecordSchema.test.ts b/tests/ArchivedRecordSchema.test.ts index ceb351459..8a9993a4c 100644 --- a/tests/ArchivedRecordSchema.test.ts +++ b/tests/ArchivedRecordSchema.test.ts @@ -95,4 +95,15 @@ describe("ArchivedAnalyticsRecordSchema", () => { expect(result.data.info.players[0].clanTag).toBe("ABC"); expect(result.data.info.players[0].stats?.conquests).toEqual([1n, 2n, 0n]); }); + + test("normalizes accidental whitespace around an archived map name", () => { + const record = oldRecord(); + record.info.config.gameMap = "Deglaciated Antarctica "; + + const result = ArchivedAnalyticsRecordSchema.safeParse(record); + + expect(result.success).toBe(true); + if (!result.success) return; + expect(result.data.info.config.gameMap).toBe("Deglaciated Antarctica"); + }); }); diff --git a/tests/client/clan/ClanApiSchemas.test.ts b/tests/client/clan/ClanApiSchemas.test.ts index d66e13ea1..4207108aa 100644 --- a/tests/client/clan/ClanApiSchemas.test.ts +++ b/tests/client/clan/ClanApiSchemas.test.ts @@ -291,6 +291,17 @@ describe("ClanGameSchema", () => { expect(ClanGameSchema.safeParse(validGame).success).toBe(true); }); + it("normalizes accidental whitespace around archived map names", () => { + const result = ClanGameSchema.safeParse({ + ...validGame, + map: "Deglaciated Antarctica ", + }); + + expect(result.success).toBe(true); + if (!result.success) return; + expect(result.data.map).toBe("Deglaciated Antarctica"); + }); + it("accepts playerTeams: null (FFA / non-team games)", () => { const result = ClanGameSchema.safeParse({ ...validGame,