diff --git a/app/Http/Controllers/HashtagFollowController.php b/app/Http/Controllers/HashtagFollowController.php index b5cc49d41..79b6e9f53 100644 --- a/app/Http/Controllers/HashtagFollowController.php +++ b/app/Http/Controllers/HashtagFollowController.php @@ -23,13 +23,16 @@ class HashtagFollowController extends Controller $user = $request->user(); $profile = $user->profile; + + abort_if(! $profile, 422, 'Profile not available for this account.'); + $tag = $request->input('name'); $hashtag = Hashtag::whereName($tag)->firstOrFail(); $hashtagFollow = HashtagFollow::firstOrCreate([ 'user_id' => $user->id, - 'profile_id' => $user->profile_id ?? $user->profile->id, + 'profile_id' => $user->profile_id ?? $profile->id, 'hashtag_id' => $hashtag->id, ]); diff --git a/tests/Feature/HashtagFollowProfileGuardTest.php b/tests/Feature/HashtagFollowProfileGuardTest.php new file mode 100644 index 000000000..6f4f54814 --- /dev/null +++ b/tests/Feature/HashtagFollowProfileGuardTest.php @@ -0,0 +1,49 @@ +profile->id when calling HashtagService. A user +| whose profile has been soft-deleted (e.g. via admin account deletion) keeps a +| live session but has a null profile relationship, so the endpoint must fail +| deterministically (422) instead of throwing a 500. +| +*/ + +it('follows a hashtag for a normal authenticated user', function () { + $user = User::factory()->create(); + $user->refresh(); + + Hashtag::create(['name' => 'landscape', 'slug' => 'landscape']); + + $this->actingAs($user) + ->postJson('/api/local/discover/tag/subscribe', ['name' => 'landscape']) + ->assertOk() + ->assertJson(['state' => 'created']); +}); + +it('returns 422 instead of 500 when the profile is soft-deleted', function () { + $user = User::factory()->create(); + $user->refresh(); + + Hashtag::create(['name' => 'landscape', 'slug' => 'landscape']); + + // Soft-delete the profile, as the admin account-deletion pipeline does, + // while leaving the still-authenticated session intact. Unset the cached + // relation so the controller re-queries and sees null, matching what a + // fresh request resolves after the pipeline runs in another request. + App\Models\Profile::whereUserId($user->id)->delete(); + $user->unsetRelation('profile'); + + $this->actingAs($user) + ->postJson('/api/local/discover/tag/subscribe', ['name' => 'landscape']) + ->assertStatus(422); +});