Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions app/Models/User.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<User>|Relation<User, Model, mixed> $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<string> $admins */
Expand Down
8 changes: 8 additions & 0 deletions tests/Feature/Controllers/Api/UserControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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));

Expand Down
17 changes: 17 additions & 0 deletions tests/Feature/Controllers/UserControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down