From cae5c08f12feec9e2fbba41dbdabb574c4962d5c Mon Sep 17 00:00:00 2001 From: Christen Dijoux Date: Sun, 18 Oct 2026 21:57:20 -0400 Subject: [PATCH] fix: find usernames in any case, like GitHub --- app/Models/User.php | 18 ++++++++++++++++++ .../Controllers/Api/UserControllerTest.php | 8 ++++++++ .../Feature/Controllers/UserControllerTest.php | 17 +++++++++++++++++ 3 files changed, 43 insertions(+) diff --git a/app/Models/User.php b/app/Models/User.php index 37f6c1f02..7afbec226 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -9,12 +9,16 @@ use App\Enums\RemotesFrom; use Carbon\CarbonInterface; use Database\Factories\UserFactory; +use Illuminate\Contracts\Database\Eloquent\Builder as BuilderContract; use Illuminate\Database\Eloquent\Attributes\Hidden; +use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Collection; use Illuminate\Database\Eloquent\Concerns\HasUuids; use Illuminate\Database\Eloquent\Factories\HasFactory; +use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Relations\BelongsToMany; use Illuminate\Database\Eloquent\Relations\HasMany; +use Illuminate\Database\Eloquent\Relations\Relation; use Illuminate\Database\Query\Builder as QueryBuilder; use Illuminate\Foundation\Auth\User as Authenticatable; use Illuminate\Support\Facades\DB; @@ -269,6 +273,20 @@ public function isOnboarded(): bool return $this->onboarded_at !== null; } + /** + * GitHub usernames are unique case-insensitively, so `/@NunoMaduro` finds `nunomaduro` like github.com does. + * + * @param Model|Builder|Relation $query + */ + public function resolveRouteBindingQuery(mixed $query, mixed $value, mixed $field = null): BuilderContract + { + if ($field === 'username' && is_string($value)) { + return $query->whereRaw('LOWER(username) = ?', [mb_strtolower($value)]); + } + + return parent::resolveRouteBindingQuery($query, $value, $field); + } + public function isAdmin(): bool { /** @var list $admins */ diff --git a/tests/Feature/Controllers/Api/UserControllerTest.php b/tests/Feature/Controllers/Api/UserControllerTest.php index 6a1a3ead2..8bebd1af0 100644 --- a/tests/Feature/Controllers/Api/UserControllerTest.php +++ b/tests/Feature/Controllers/Api/UserControllerTest.php @@ -68,3 +68,11 @@ it('returns not found for unknown usernames', function (): void { $this->getJson('/api/v1/users/missing')->assertNotFound(); }); + +it('finds a username in any case, like github', function (): void { + User::factory()->create(['username' => 'NunoMaduro']); + + $this->getJson('/api/v1/users/nunomaduro') + ->assertOk() + ->assertJsonPath('data.username', 'NunoMaduro'); +}); diff --git a/tests/Feature/Controllers/UserControllerTest.php b/tests/Feature/Controllers/UserControllerTest.php index 912104fcb..609dd5af1 100644 --- a/tests/Feature/Controllers/UserControllerTest.php +++ b/tests/Feature/Controllers/UserControllerTest.php @@ -307,6 +307,23 @@ $response->assertNotFound(); }); +it('finds the profile and commits of a username in any case, like github', function (): void { + $user = User::factory()->create(['username' => 'NunoMaduro']); + $commit = Commit::factory()->for($user)->create(); + $viewer = User::factory()->create(); + + foreach (['/@nunomaduro', '/@NUNOMADURO', '/@NunoMaduro'] as $url) { + $this->actingAs($viewer) + ->get($url) + ->assertOk() + ->assertInertia(fn (AssertableInertia $page): AssertableInertia => $page->where('user.username', 'NunoMaduro')); + } + + $this->actingAs($viewer) + ->get('/@nunomaduro/commits/'.$commit->id) + ->assertOk(); +}); + it('shows guests the profile with its approved commits and a link preview', function (): void { $user = User::factory()->create([ 'username' => 'octocat',