From 1599dfd5158b20d098c1bf52e48b0d778f50e324 Mon Sep 17 00:00:00 2001 From: Yash Singh <123385188+yashs33244@users.noreply.github.com> Date: Fri, 19 Jun 2026 11:07:42 +0530 Subject: [PATCH] fix: zero-pad username suggestion suffix consistently (#29571) generateUsernameSuggestion built its numeric suffix with String(rand).padStart(4 - rand.toString().length, "0"). padStart's first argument is the target total length, not the number of pad characters, so the expression only padded single-digit numbers: rand=1 became "001" but rand=42 became "42" and rand=999 stayed "999". Suggested usernames therefore had inconsistent suffix widths, and the magic 4 didn't match the 3-wide output the existing test pins ("johnny001"). Pull the suffix into a small helper that always pads to 3 digits, so collisions and the returned value share one definition and every suggestion is uniformly zero-padded. Add unit tests for the first-attempt path and for two- and three-digit suffixes; the two-digit case fails on the old code. Co-authored-by: Bandhan Majumder <133476557+bandhan-majumder@users.noreply.github.com> --- packages/lib/server/username.test.ts | 31 ++++++++++++++++++++++++++-- packages/lib/server/username.ts | 5 +++-- 2 files changed, 32 insertions(+), 4 deletions(-) diff --git a/packages/lib/server/username.test.ts b/packages/lib/server/username.test.ts index 3d3b653a8c..87ec703fa9 100644 --- a/packages/lib/server/username.test.ts +++ b/packages/lib/server/username.test.ts @@ -1,8 +1,8 @@ import prismaMock from "@calcom/testing/lib/__mocks__/prismaMock"; -import { describe, expect, it, beforeEach } from "vitest"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -import { usernameCheckForSignup } from "./username"; +import { generateUsernameSuggestion, usernameCheckForSignup } from "./username"; describe("usernameCheckForSignup ", async () => { beforeEach(() => { @@ -57,6 +57,33 @@ describe("usernameCheckForSignup ", async () => { }); }); +describe("generateUsernameSuggestion", () => { + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("appends a zero-padded suffix when the username is free on the first attempt", async () => { + const suggestion = await generateUsernameSuggestion([], "john"); + expect(suggestion).toBe("john001"); + }); + + it("zero-pads a two-digit suffix to a consistent width", async () => { + // Force the first candidate (john001) to collide so a random suffix is generated, + // and pin Math.random so the generated suffix is the two-digit number 42. + // username length >= 2 -> limit 999 -> rand = ceil(1 + random * 998); 40.5/998 -> ceil(41.5) = 42. + vi.spyOn(Math, "random").mockReturnValue(40.5 / 998); + const suggestion = await generateUsernameSuggestion(["john001"], "john"); + expect(suggestion).toBe("john042"); + }); + + it("zero-pads a three-digit suffix to a consistent width", async () => { + // 122.5/998 -> ceil(1 + 122.5) = ceil(123.5) = 124. + vi.spyOn(Math, "random").mockReturnValue(122.5 / 998); + const suggestion = await generateUsernameSuggestion(["john001"], "john"); + expect(suggestion).toBe("john124"); + }); +}); + function mockUserInDB({ id, email, username }: { id: number; email: string; username: string }) { // @ts-expect-error prismaMock.user.findUnique.mockImplementation((arg) => { diff --git a/packages/lib/server/username.ts b/packages/lib/server/username.ts index 9b3cfc16fe..7ad801fdf6 100644 --- a/packages/lib/server/username.ts +++ b/packages/lib/server/username.ts @@ -57,11 +57,12 @@ export const isPremiumUserName = IS_PREMIUM_USERNAME_ENABLED export const generateUsernameSuggestion = async (users: string[], username: string) => { const limit = username.length < 2 ? 9999 : 999; + const suffix = (value: number) => String(value).padStart(3, "0"); let rand = 1; - while (users.includes(username + String(rand).padStart(4 - rand.toString().length, "0"))) { + while (users.includes(username + suffix(rand))) { rand = Math.ceil(1 + Math.random() * (limit - 1)); } - return username + String(rand).padStart(4 - rand.toString().length, "0"); + return username + suffix(rand); }; const processResult = (