Files
QuickandClaude Opus 4.7 c61381dc79 feat(auth): make usernames and emails case-insensitive
Login identifiers were compared case-sensitively, so "John" and "john"
were distinct accounts and a user could not log in unless they matched the
exact casing used at registration.

Convert users.username and users.email to the citext type (migration 165).
citext compares case-insensitively while preserving the originally stored
casing for display, so the existing unique constraints become
case-insensitive and `WHERE username = $1` / `email = $1` lookups match
regardless of case with no change to the query code itself.

Also add auth.NormalizeUsername/NormalizeEmail (trim-only; case preserved),
applied at the repository chokepoints (Create, Update, GetByUsername,
GetByEmail) and before validation in the create paths, so surrounding
whitespace no longer defeats matching or creates lookalike accounts.

Verified non-destructively against the dev DB: mixed-case lookups resolve
to the same row, case-variant inserts are rejected by the unique
constraint, and the down migration cleanly reverts to text.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-28 21:44:51 -04:00

47 lines
1.3 KiB
Go

package auth
import "testing"
func TestNormalizeUsername(t *testing.T) {
cases := []struct {
name string
in string
want string
}{
{"trims surrounding spaces", " john ", "john"},
{"trims tabs and newlines", "\t john\n", "john"},
{"preserves internal spacing", "john doe", "john doe"},
{"preserves case", "JohnDoe", "JohnDoe"},
{"whitespace only becomes empty", " ", ""},
{"already clean is unchanged", "john", "john"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := NormalizeUsername(tc.in); got != tc.want {
t.Errorf("NormalizeUsername(%q) = %q, want %q", tc.in, got, tc.want)
}
})
}
}
func TestNormalizeEmail(t *testing.T) {
cases := []struct {
name string
in string
want string
}{
{"trims surrounding spaces", " user@example.com ", "user@example.com"},
{"trims tabs and newlines", "\tuser@example.com\n", "user@example.com"},
{"preserves case (not lowercased)", "User@Example.COM", "User@Example.COM"},
{"whitespace only becomes empty", " ", ""},
{"already clean is unchanged", "user@example.com", "user@example.com"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
if got := NormalizeEmail(tc.in); got != tc.want {
t.Errorf("NormalizeEmail(%q) = %q, want %q", tc.in, got, tc.want)
}
})
}
}