Fix static analysis warning and align timeline tests with live-feed fanout

Drop an always-true empty() guard on the home feed Link header that static
analysis flagged as redundant.

Update the fanout and follow-backfill tests to seed a live home feed before
dispatching, since fanout now only appends to feeds that already exist and
skips idle ones. Switch the network null-account test to a real sorted set
instead of a Redis spy so the cached read path actually runs, and force a
deterministic id in the chronological ordering test so it no longer depends
on sub-millisecond insert timing.
pull/7759/head
Ashley 2 weeks ago
parent 5d1d2ecea4
commit 0c89464d5c

@ -2934,7 +2934,7 @@ class ApiV1Controller extends Controller
$pageIds = $res->pluck('id');
$headers['Link'] = '<'.$baseUrl.'max_id='.$pageIds->min().'>; rel="next",<'.$baseUrl.'min_id='.$pageIds->max().'>; rel="prev"';
} elseif (! empty($ids)) {
} else {
$headers['Link'] = '<'.$baseUrl.'max_id='.collect($ids)->map(fn ($id) => (int) $id)->min().'>; rel="next"';
}

@ -4,6 +4,7 @@ use App\Models\Profile;
use App\Models\Status;
use App\Models\User;
use App\Services\AccountService;
use App\Services\NetworkTimelineService;
use Illuminate\Foundation\Testing\LazilyRefreshDatabase;
use Illuminate\Support\Facades\Redis;
@ -14,16 +15,19 @@ uses(LazilyRefreshDatabase::class);
| GET /api/pixelfed/v1/timelines/network null-account filtering
|--------------------------------------------------------------------------
|
| The non-cached network timeline path must drop statuses whose account failed
| to resolve (e.g. a deleted remote profile) so it never returns account=null,
| matching the cached path and the Mastodon API contract.
| The network timeline must drop statuses whose account failed to resolve
| (e.g. a deleted remote profile) so it never returns account=null, matching
| the Mastodon API contract.
|
*/
beforeEach(function () {
Redis::spy();
config(['federation.network_timeline' => true]);
config(['instance.timeline.network.cached' => false]);
Redis::del(NetworkTimelineService::CACHE_KEY);
});
afterEach(function () {
Redis::del(NetworkTimelineService::CACHE_KEY);
});
function remotePublicStatus(Profile $author): Status
@ -34,7 +38,7 @@ function remotePublicStatus(Profile $author): Status
'scope' => 'public',
'visibility' => 'public',
'local' => false,
'uri' => 'https://remote.example/p/'.uniqid(),
'uri' => 'https://remote.example/p/'.bin2hex(random_bytes(6)),
]);
}
@ -48,6 +52,10 @@ it('excludes statuses whose account failed to resolve', function () {
$liveStatus = remotePublicStatus($liveAuthor);
$deletedStatus = remotePublicStatus($deletedAuthor);
// Both posts are present in the cached feed.
NetworkTimelineService::add($liveStatus->id);
NetworkTimelineService::add($deletedStatus->id);
// Mark the second author deleted so AccountService::get() returns null.
$deletedAuthor->status = 'delete';
$deletedAuthor->save();

@ -36,6 +36,16 @@ function blockProfile(int $userProfileId, int $blockedProfileId): void
$filter->save();
}
function publicPostBy(User $author): Status
{
return Status::factory()->create([
'profile_id' => $author->profile_id,
'type' => 'photo',
'scope' => 'public',
'visibility' => 'public',
]);
}
function reblogStatus(User $original, User $sharer): Status
{
$orig = Status::factory()->create([
@ -69,6 +79,10 @@ it('skips a follower who blocked the reblogged posts original author', function
FollowerService::add($viewer->profile_id, $sharer->profile_id);
blockProfile($viewer->profile_id, $original->profile_id);
// Fan-out only appends to a live feed, so give the viewer one.
$seed = publicPostBy($viewer);
HomeTimelineService::add($viewer->profile_id, $seed->id);
$boost = reblogStatus($original, $sharer);
StatusService::get($boost->id, false);
@ -91,6 +105,10 @@ it('delivers a reblog to a follower who blocked nobody', function () {
Follower::create(['profile_id' => $viewer->profile_id, 'following_id' => $sharer->profile_id, 'local_profile' => true]);
FollowerService::add($viewer->profile_id, $sharer->profile_id);
// Fan-out only appends to a live feed, so give the viewer one.
$seed = publicPostBy($viewer);
HomeTimelineService::add($viewer->profile_id, $seed->id);
$boost = reblogStatus($original, $sharer);
StatusService::get($boost->id, false);

@ -52,6 +52,10 @@ it('does not backfill any posts when the actor does not follow the target', func
Redis::del(HomeTimelineService::CACHE_KEY.$actor->profile_id);
// Backfill only appends to a live feed, so give the actor one.
$seed = publicPost($actor);
HomeTimelineService::add($actor->profile_id, $seed->id);
$priv = privatePost($target);
$pub = publicPost($target);
@ -71,6 +75,10 @@ it('backfills private posts for an accepted follower', function () {
Redis::del(HomeTimelineService::CACHE_KEY.$actor->profile_id);
// Backfill only appends to a live feed, so give the actor one.
$seed = publicPost($actor);
HomeTimelineService::add($actor->profile_id, $seed->id);
Follower::create(['profile_id' => $actor->profile_id, 'following_id' => $target->profile_id, 'local_profile' => true]);
FollowerService::add($actor->profile_id, $target->profile_id);

@ -35,8 +35,11 @@ it('ranks home posts by created_at even when a remote post has a newer id but ol
'created_at' => now()->subHour(),
]);
// A remote post ingested AFTER (higher id) but published weeks earlier.
// A remote post ingested AFTER (higher id) but published weeks earlier. The
// id is forced above the recent one so the ordering does not depend on the
// sub-millisecond gap between two factory inserts.
$staleRemote = Status::factory()->create([
'id' => $recent->id + 1000,
'profile_id' => $remoteAuthor->id,
'type' => 'photo', 'scope' => 'public', 'visibility' => 'public',
'in_reply_to_id' => null, 'reblog_of_id' => null,

Loading…
Cancel
Save