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.
This commit is contained in:
@@ -53,6 +53,7 @@ const sensitiveRateLimitConfig = {
|
|||||||
const registerSchema = z.object({
|
const registerSchema = z.object({
|
||||||
username: z
|
username: z
|
||||||
.string()
|
.string()
|
||||||
|
.trim()
|
||||||
.min(3, "Username must be at least 3 characters")
|
.min(3, "Username must be at least 3 characters")
|
||||||
.max(50, "Username must be at most 50 characters")
|
.max(50, "Username must be at most 50 characters")
|
||||||
.regex(/^[a-zA-Z0-9_-]+$/, "Username can only contain letters, numbers, underscores, and hyphens"),
|
.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({
|
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"),
|
password: z.string().min(1, "Password is required"),
|
||||||
rememberMe: z.boolean().optional().default(false),
|
rememberMe: z.boolean().optional().default(false),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -245,6 +245,57 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => {
|
|||||||
expect(response.json().code).toBe("VALIDATION_ERROR");
|
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 () => {
|
it("should reject invalid username characters", async () => {
|
||||||
const response = await app.inject({
|
const response = await app.inject({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -341,6 +392,35 @@ describe("Auth Routes (AUTH_ENABLED=true)", () => {
|
|||||||
expect(response.json().code).toBe("INVALID_CREDENTIALS");
|
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 () => {
|
it("should support rememberMe option", async () => {
|
||||||
const response = await app.inject({
|
const response = await app.inject({
|
||||||
method: "POST",
|
method: "POST",
|
||||||
|
|||||||
Reference in New Issue
Block a user