Skip to content
Merged
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
2 changes: 1 addition & 1 deletion KNOWLEDGE_BASE.md
Original file line number Diff line number Diff line change
Expand Up @@ -534,7 +534,7 @@ A signed request sees what the web shows you, such as your own pending commits.
- preview commit drafts without publishing them;
- follow or unfollow, mute or unmute, and block or unblock, and list who you muted and blocked;
- read who to follow, also as it is suggested beside a profile or a commit, and mark a suggestion not interested;
- star or unstar;
- star or unstar, and list who starred your own commit, newest first, 20 a page;
- create polls on commits that are neither replies nor quotes, and vote on polls;
- change Remotes settings, read and send messages with images, link cards, and voice notes in conversations and orgs, mark them pulled, react to messages and list your reactions, pin and unpin org messages as a maintainer, find people, and start orgs;
- rename orgs, add people to them, change their roles, remove them, and leave an org, with the same maintainer rules as the web;
Expand Down
15 changes: 15 additions & 0 deletions app/Http/Controllers/Api/CommitStarController.php
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,29 @@
use App\Actions\DeleteCommitStar;
use App\Http\Requests\CreateApiCommitStarRequest;
use App\Http\Requests\DeleteApiCommitStarRequest;
use App\Http\Requests\ListApiCommitStarsRequest;
use App\Http\Resources\CommitResource;
use App\Http\Resources\UserResource;
use App\Models\Commit;
use App\Models\User;
use App\Queries\CommitPageQuery;
use Illuminate\Container\Attributes\CurrentUser;
use Illuminate\Http\Resources\Json\AnonymousResourceCollection;

final readonly class CommitStarController
{
/**
* Who starred the commit, newest first, 20 a page as on the web. People across a block are left out.
*/
public function index(ListApiCommitStarsRequest $request, #[CurrentUser] User $user, Commit $commit): AnonymousResourceCollection
{
return UserResource::collection($commit->stars()
->whereNotIn('users.id', $user->idsAcrossBlocks())
->latest('stars.created_at')
->latest('users.id')
->cursorPaginate(20));
}

/**
* Answers with the commit as its page shows it, so the app gets the new stars_count and starred.
*/
Expand Down
35 changes: 35 additions & 0 deletions app/Http/Requests/ListApiCommitStarsRequest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

declare(strict_types=1);

namespace App\Http\Requests;

use App\Models\Commit;
use App\Models\User;
use Illuminate\Auth\Access\Response;
use Illuminate\Foundation\Http\FormRequest;

final class ListApiCommitStarsRequest extends FormRequest
{
/**
* Only authors see who starred their commit, as on the web. Everyone else only sees how many did.
*/
public function authorize(): Response
{
$user = $this->user();
assert($user instanceof User);

$commit = $this->route('commit');
assert($commit instanceof Commit);

return $commit->user_id === $user->id ? Response::allow() : Response::denyAsNotFound();
}

/**
* @return array{}
*/
public function rules(): array
{
return [];
}
}
2 changes: 2 additions & 0 deletions routes/api.php
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,8 @@
Route::get('starred', [StarredCommitController::class, 'index'])->name('api-starred.index');

// Commit Star...
Route::get('commits/{commit}/stars', [CommitStarController::class, 'index'])
->name('api-commit-star.index');
Route::post('commits/{commit}/star', [CommitStarController::class, 'store'])
->middleware('throttle:stars')
->name('api-commit-star.store');
Expand Down
66 changes: 66 additions & 0 deletions tests/Feature/Controllers/Api/CommitStarControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -183,3 +183,69 @@
->assertTooManyRequests()
->assertExactJson(['message' => 'Too Many Attempts.', 'code' => 'rate_limited']);
});

it('lists who starred their commit, newest first, as the web does', function (): void {
$commit = Commit::factory()->for($this->user)->create();
$first = User::factory()->create(['handle' => 'first']);
$second = User::factory()->create(['handle' => 'second']);

$first->starredCommits()->attach($commit, ['created_at' => now()->subMinute()]);
$second->starredCommits()->attach($commit);
User::factory()->create()->starredCommits()->attach(Commit::factory()->for($this->user)->create());

$response = signedApiRequest($this->keyPair, 'GET', route('api-commit-star.index', $commit));

$response->assertOk()
->assertJsonPath('data.0.id', $second->id)
->assertJsonPath('data.0.handle', 'second')
->assertJsonPath('data.0.avatar_url', $second->avatar_url)
->assertJsonPath('data.0.lane', $second->lane())
->assertJsonMissingPath('data.0.pivot')
->assertJsonMissingPath('data.0.email')
->assertJsonPath('data.1.handle', 'first')
->assertJsonCount(2, 'data')
->assertJsonPath('meta.next_cursor', null);
});

it('lists who starred their commit 20 a page', function (): void {
$commit = Commit::factory()->for($this->user)->create();

User::factory()->count(21)->create()->each(fn (User $stargazer) => $stargazer->starredCommits()->attach($commit));

$page = signedApiRequest($this->keyPair, 'GET', route('api-commit-star.index', $commit))
->assertOk()
->assertJsonCount(20, 'data');

signedApiRequest($this->keyPair, 'GET', route('api-commit-star.index', ['commit' => $commit, 'cursor' => $page->json('meta.next_cursor')]))
->assertOk()
->assertJsonCount(1, 'data')
->assertJsonPath('meta.next_cursor', null);
});

it('leaves out who starred their commit across a block in either direction', function (bool $userBlocks): void {
$commit = Commit::factory()->for($this->user)->create();
$blocked = User::factory()->create();

$blocked->starredCommits()->attach($commit);
$userBlocks ? $this->user->blocking()->attach($blocked) : $blocked->blocking()->attach($this->user);

signedApiRequest($this->keyPair, 'GET', route('api-commit-star.index', $commit))
->assertOk()
->assertJsonCount(0, 'data');
})->with([true, false]);

it('may not list who starred the commit of someone else', function (): void {
$commit = Commit::factory()->create();

User::factory()->create()->starredCommits()->attach($commit);

signedApiRequest($this->keyPair, 'GET', route('api-commit-star.index', $commit))
->assertNotFound();
});

it('requires a signed request to list who starred a commit', function (): void {
$commit = Commit::factory()->for($this->user)->create();

$this->getJson(route('api-commit-star.index', $commit))
->assertUnauthorized();
});
Loading