diff --git a/app/Models/User.php b/app/Models/User.php index 085bb8f76..3b4aa20a9 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -12,13 +12,17 @@ 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\HasOne; +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; @@ -370,6 +374,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 5212ea033..f56e73321 100644 --- a/tests/Feature/Controllers/Api/UserControllerTest.php +++ b/tests/Feature/Controllers/Api/UserControllerTest.php @@ -72,6 +72,14 @@ $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'); +}); + it('shows a signed request the profile as the web shows it to its user', function (): void { $this->travelTo(now()->setDate(2026, 10, 15)->setTime(12, 0)); diff --git a/tests/Feature/Controllers/UserControllerTest.php b/tests/Feature/Controllers/UserControllerTest.php index f98d5895f..eb39f4fab 100644 --- a/tests/Feature/Controllers/UserControllerTest.php +++ b/tests/Feature/Controllers/UserControllerTest.php @@ -322,6 +322,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',