From d0e2ee078300f5deb97d467564fef1de0ced21e5 Mon Sep 17 00:00:00 2001 From: Daniel Volz Date: Sun, 22 Feb 2026 17:51:41 +0100 Subject: [PATCH] fix: trim whitespace from username on login and registration (#277) Add .trim() to both loginSchema and registerSchema Zod validators so leading/trailing spaces are stripped before validation and DB lookup. Includes 5 new test cases covering trim behavior for both endpoints. --- backend/src/routes/auth.ts | 3 +- backend/src/test/auth.test.ts | 80 +++++++++++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 01b3474..ea9240a 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -53,6 +53,7 @@ const sensitiveRateLimitConfig = { const registerSchema = z.object({ username: z .string() + .trim() .min(3, "Username must be at least 3 characters") .max(50, "Username must be at most 50 characters") .regex(/^[a-zA-Z0-9_-]+$/, "Username can only contain letters, numbers, underscores, and hyphens"), @@ -63,7 +64,7 @@ const registerSchema = z.object({ }); const loginSchema = z.object({ - username: z.string().min(1, "Username is required"), + username: z.string().trim().min(1, "Username is required"), password: z.string().min(1, "Password is required"), rememberMe: z.boolean().optional().default(false), }); diff --git a/backend/src/test/auth.test.ts b/backend/src/test/auth.test.ts index 9a34615..b791c5c 100644 --- a/backend/src/test/auth.test.ts +++ b/backend/src/test/auth.test.ts @@ -245,6 +245,57 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => { expect(response.json().code).toBe("VALIDATION_ERROR"); }); + it("should register with trimmed username when input has whitespace", async () => { + const response = await app.inject({ + method: "POST", + url: "/auth/register", + payload: { + username: " trimuser ", + password: "TestPassword123", + }, + }); + + expect(response.statusCode).toBe(201); + expect(response.json().user.username).toBe("trimuser"); + }); + + it("should reject whitespace-only username on registration", async () => { + const response = await app.inject({ + method: "POST", + url: "/auth/register", + payload: { + username: " ", + password: "TestPassword123", + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json().code).toBe("VALIDATION_ERROR"); + }); + + it("should reject duplicate username even with surrounding whitespace", async () => { + await app.inject({ + method: "POST", + url: "/auth/register", + payload: { + username: "spacedupe", + password: "TestPassword123", + }, + }); + + const response = await app.inject({ + method: "POST", + url: "/auth/register", + payload: { + username: " spacedupe ", + password: "AnotherPassword123", + }, + }); + + expect(response.statusCode).toBe(409); + expect(response.json().code).toBe("USERNAME_EXISTS"); + }); + it("should reject invalid username characters", async () => { const response = await app.inject({ method: "POST", @@ -341,6 +392,35 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => { expect(response.json().code).toBe("INVALID_CREDENTIALS"); }); + it("should login successfully when username has leading/trailing whitespace", async () => { + const response = await app.inject({ + method: "POST", + url: "/auth/login", + payload: { + username: " loginuser ", + password: "TestPassword123", + }, + }); + + expect(response.statusCode).toBe(200); + expect(response.json().ok).toBe(true); + expect(response.json().user.username).toBe("loginuser"); + }); + + it("should reject whitespace-only username on login", async () => { + const response = await app.inject({ + method: "POST", + url: "/auth/login", + payload: { + username: " ", + password: "TestPassword123", + }, + }); + + expect(response.statusCode).toBe(400); + expect(response.json().code).toBe("VALIDATION_ERROR"); + }); + it("should support rememberMe option", async () => { const response = await app.inject({ method: "POST",