diff --git a/app/Http/Controllers/PublicApiController.php b/app/Http/Controllers/PublicApiController.php index ab8540282..221fe2ddb 100644 --- a/app/Http/Controllers/PublicApiController.php +++ b/app/Http/Controllers/PublicApiController.php @@ -478,7 +478,7 @@ class PublicApiController extends Controller return $status; }) ->filter(function ($s) use ($filtered) { - return $s && in_array($s['account']['id'], $filtered) == false; + return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false; }) ->values() ->toArray(); @@ -527,7 +527,7 @@ class PublicApiController extends Controller return $status; }) ->filter(function ($s) use ($filtered) { - return $s && in_array($s['account']['id'], $filtered) == false; + return $s && isset($s['account']) && in_array($s['account']['id'], $filtered) == false; }) ->values() ->toArray(); diff --git a/tests/Feature/Api/HomeTimelineNullAccountTest.php b/tests/Feature/Api/HomeTimelineNullAccountTest.php new file mode 100644 index 000000000..2a0fe8e6f --- /dev/null +++ b/tests/Feature/Api/HomeTimelineNullAccountTest.php @@ -0,0 +1,119 @@ + HTTP 500), matching +| every other timeline filter in the controller. +| +*/ + +beforeEach(function () { + Redis::spy(); +}); + +/** + * Make $viewer follow $author. + */ +function follow(int $viewerPid, int $authorPid): void +{ + $f = new Follower; + $f->profile_id = $viewerPid; + $f->following_id = $authorPid; + $f->save(); +} + +function homeStatus(int $authorPid): Status +{ + return Status::factory()->create([ + 'profile_id' => $authorPid, + 'type' => 'photo', + 'scope' => 'public', + 'visibility' => 'public', + ]); +} + +it('returns 200 and drops a followed status whose author was deleted', function () { + $viewer = User::factory()->create(); + $viewer->refresh(); + + $liveAuthor = User::factory()->create(); + $liveAuthor->refresh(); + $deletedAuthor = User::factory()->create(); + $deletedAuthor->refresh(); + + follow($viewer->profile_id, $liveAuthor->profile_id); + follow($viewer->profile_id, $deletedAuthor->profile_id); + + $liveStatus = homeStatus($liveAuthor->profile_id); + $deletedStatus = homeStatus($deletedAuthor->profile_id); + + // Warm the status cache while both authors resolve. + StatusService::get($liveStatus->id, false); + StatusService::get($deletedStatus->id, false); + + // Delete the second author's profile so AccountService::get() returns null, + // making StatusService::get() return account=null for its status. + $deletedAuthor->profile->status = 'delete'; + $deletedAuthor->profile->save(); + AccountService::del($deletedAuthor->profile_id); + StatusService::del($deletedStatus->id); + + $res = $this->actingAs($viewer) + ->getJson('/api/pixelfed/v1/timelines/home?limit=40') + ->assertOk() + ->json(); + + $ids = collect($res)->pluck('id')->map(fn ($id) => (string) $id)->all(); + + // No entry has a null account. + foreach ($res as $entry) { + expect($entry['account'] ?? null)->not->toBeNull(); + } + + // The deleted-author status is filtered out; the live one remains. + expect($ids)->not->toContain((string) $deletedStatus->id); + expect($ids)->toContain((string) $liveStatus->id); +}); + +it('returns 200 for the min_id/max_id branch when an author was deleted', function () { + $viewer = User::factory()->create(); + $viewer->refresh(); + + $deletedAuthor = User::factory()->create(); + $deletedAuthor->refresh(); + + follow($viewer->profile_id, $deletedAuthor->profile_id); + + $deletedStatus = homeStatus($deletedAuthor->profile_id); + StatusService::get($deletedStatus->id, false); + + $deletedAuthor->profile->status = 'delete'; + $deletedAuthor->profile->save(); + AccountService::del($deletedAuthor->profile_id); + StatusService::del($deletedStatus->id); + + // max_id exercises the ($min || $max) branch (the other filter callback). + $res = $this->actingAs($viewer) + ->getJson('/api/pixelfed/v1/timelines/home?limit=40&max_id='.($deletedStatus->id + 1)) + ->assertOk() + ->json(); + + $ids = collect($res)->pluck('id')->map(fn ($id) => (string) $id)->all(); + expect($ids)->not->toContain((string) $deletedStatus->id); +});