brackt/app/routes/__tests__/onboarding.test.ts
Chris Parsons 215b0cf6b5
Add user onboarding flow with username selection (#430)
* Add username onboarding prompt for new signups

New users (especially via OAuth/Google) are redirected to /onboarding
to choose a display username before accessing the app. Adds unique
constraint on users.username and case-insensitive uniqueness validation.

https://claude.ai/code/session_01UinBMAy9d6dQzzbcUAdq8J

* Address all code review issues on username onboarding

- Replace case-sensitive unique constraint with functional unique index on
  lower(username) so DB-level uniqueness is case-insensitive (fix #1)
- Share USERNAME_RE from models/user.ts; add same format + uniqueness
  validation to the settings update-profile action (fix #2)
- Wrap updateUser calls in try/catch to handle race-condition DB errors
  gracefully in both onboarding and settings (fix #3)
- Add unit tests for suggestUsername, safeRedirectTo, USERNAME_RE, and
  isOnboardingExempt (fix #4)
- Guard suggestUsername against returning strings shorter than 3 chars (fix #5)
- Preserve the user's intended destination via redirectTo query param
  through the onboarding flow (fix #6)
- Treat empty username in settings as a validation error instead of a
  silent no-op (fix #7)
- Use exact/sub-path matching in isOnboardingExempt to prevent
  false-positive prefix matches like /loginpage (fix #8)

https://claude.ai/code/session_01UinBMAy9d6dQzzbcUAdq8J

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-05-15 11:17:15 -07:00

98 lines
2.9 KiB
TypeScript

import { describe, it, expect } from "vitest";
import { suggestUsername, safeRedirectTo } from "../onboarding";
import { USERNAME_RE } from "~/models/user";
describe("suggestUsername", () => {
it("returns empty string for null", () => {
expect(suggestUsername(null)).toBe("");
});
it("returns empty string for empty string", () => {
expect(suggestUsername("")).toBe("");
});
it("lowercases and strips spaces", () => {
expect(suggestUsername("John Smith")).toBe("johnsmith");
});
it("strips special characters", () => {
expect(suggestUsername("Jane O'Brien!")).toBe("janeobrien");
});
it("preserves underscores and hyphens", () => {
expect(suggestUsername("cool_user-name")).toBe("cool_user-name");
});
it("slices to 20 characters", () => {
expect(suggestUsername("averylongdisplaynamethatexceedstwenty")).toBe("averylongdisplayname");
expect(suggestUsername("averylongdisplaynamethatexceedstwenty").length).toBeLessThanOrEqual(20);
});
it("returns empty string when cleaned result is shorter than 3 characters", () => {
expect(suggestUsername("Jo")).toBe("");
expect(suggestUsername("AB")).toBe("");
});
it("returns the suggestion when cleaned result is exactly 3 characters", () => {
expect(suggestUsername("abc")).toBe("abc");
});
it("returns empty string for non-ASCII display names that strip to nothing", () => {
expect(suggestUsername("小林")).toBe("");
});
});
describe("safeRedirectTo", () => {
it("returns / for null", () => {
expect(safeRedirectTo(null)).toBe("/");
});
it("returns / for empty string", () => {
expect(safeRedirectTo("")).toBe("/");
});
it("returns / for bare slash", () => {
expect(safeRedirectTo("/")).toBe("/");
});
it("allows a root-relative path", () => {
expect(safeRedirectTo("/leagues/123/draft/456")).toBe("/leagues/123/draft/456");
});
it("rejects an absolute URL", () => {
expect(safeRedirectTo("https://evil.com")).toBe("/");
});
it("rejects a protocol-relative URL", () => {
expect(safeRedirectTo("//evil.com")).toBe("/");
});
});
describe("USERNAME_RE", () => {
it("allows 3-character usernames", () => {
expect(USERNAME_RE.test("abc")).toBe(true);
});
it("allows 30-character usernames", () => {
expect(USERNAME_RE.test("a".repeat(30))).toBe(true);
});
it("allows letters, numbers, underscores, and hyphens", () => {
expect(USERNAME_RE.test("Cool_User-123")).toBe(true);
});
it("rejects usernames shorter than 3 characters", () => {
expect(USERNAME_RE.test("ab")).toBe(false);
expect(USERNAME_RE.test("")).toBe(false);
});
it("rejects usernames longer than 30 characters", () => {
expect(USERNAME_RE.test("a".repeat(31))).toBe(false);
});
it("rejects special characters", () => {
expect(USERNAME_RE.test("user!")).toBe(false);
expect(USERNAME_RE.test("user name")).toBe(false);
expect(USERNAME_RE.test("user@host")).toBe(false);
});
});