create(); $blocked = User::factory()->create(); $blocker->following()->attach($blocked, ['status' => FollowStatus::Accepted]); $blocked->following()->attach($blocker, ['status' => FollowStatus::Accepted]); Sanctum::actingAs($blocker); $this->postJson("/api/users/{$blocked->id}/block") ->assertOk() ->assertJsonPath('code', 'USER_BLOCKED'); $this->postJson("/api/users/{$blocked->id}/block")->assertOk(); expect(UserBlock::query()->count())->toBe(1) ->and($blocker->following()->count())->toBe(0) ->and($blocker->followers()->count())->toBe(0); $this->getJson('/api/me/blocked-users') ->assertOk() ->assertJsonCount(1, 'data') ->assertJsonPath('data.0.id', $blocked->id); $this->deleteJson("/api/users/{$blocked->id}/block")->assertNoContent(); expect(UserBlock::query()->count())->toBe(0); }); it('does not allow a user to block themselves', function () { $user = User::factory()->create(); Sanctum::actingAs($user); $this->postJson("/api/users/{$user->id}/block") ->assertUnprocessable() ->assertJsonPath('code', 'CANNOT_BLOCK_SELF'); }); it('requires authentication to manage blocks', function () { $user = User::factory()->create(); $this->getJson('/api/me/blocked-users')->assertUnauthorized(); $this->postJson("/api/users/{$user->id}/block")->assertUnauthorized(); $this->deleteJson("/api/users/{$user->id}/block")->assertUnauthorized(); }); it('hides blocked users and prevents reciprocal content interactions', function () { $viewer = User::factory()->create(); $blocked = User::factory()->create(); $neutral = User::factory()->create(); $blockedMeal = MealPosts::factory()->for($blocked, 'user')->create(); $neutralMeal = MealPosts::factory()->for($neutral, 'user')->create(); $blockedReview = PostReviews::query()->create([ 'meal_post_id' => $neutralMeal->id, 'user_id' => $blocked->id, 'rating' => 4, 'comment' => 'Avis masqué.', ]); UserBlock::factory()->create([ 'blocker_id' => $viewer->id, 'blocked_id' => $blocked->id, ]); Sanctum::actingAs($viewer); $this->getJson('/api/meal-posts') ->assertOk() ->assertJsonFragment(['id' => $neutralMeal->id]) ->assertJsonMissing(['id' => $blockedMeal->id]); $this->getJson("/api/users/{$blocked->id}")->assertNotFound(); $this->getJson("/api/meal-posts/{$blockedMeal->id}")->assertNotFound(); $this->postJson("/api/users/{$blocked->id}/follow")->assertNotFound(); $this->postJson("/api/meal-posts/{$blockedMeal->id}/like")->assertNotFound(); $this->postJson("/api/meal-posts/{$blockedMeal->id}/reviews", [ 'rating' => 5, 'comment' => 'Interaction interdite.', ])->assertNotFound(); $this->getJson("/api/meal-posts/{$neutralMeal->id}/reviews") ->assertOk() ->assertJsonMissing(['id' => $blockedReview->id]); }); it('applies reciprocal exclusion when the other user initiated the block', function () { $viewer = User::factory()->create(); $blocker = User::factory()->create(); $meal = MealPosts::factory()->for($blocker, 'user')->create(); UserBlock::factory()->create([ 'blocker_id' => $blocker->id, 'blocked_id' => $viewer->id, ]); Sanctum::actingAs($viewer); $this->getJson('/api/meal-posts') ->assertOk() ->assertJsonMissing(['id' => $meal->id]); $this->getJson("/api/users/{$blocker->id}")->assertNotFound(); $this->getJson("/api/meal-posts/{$meal->id}")->assertNotFound(); $this->postJson("/api/users/{$blocker->id}/follow")->assertNotFound(); }); it('keeps content hidden until both directional blocks are removed', function () { $firstUser = User::factory()->create(); $secondUser = User::factory()->create(); $meal = MealPosts::factory()->for($secondUser, 'user')->create(); UserBlock::factory()->create([ 'blocker_id' => $firstUser->id, 'blocked_id' => $secondUser->id, ]); UserBlock::factory()->create([ 'blocker_id' => $secondUser->id, 'blocked_id' => $firstUser->id, ]); Sanctum::actingAs($firstUser); $this->deleteJson("/api/users/{$secondUser->id}/block")->assertNoContent(); $this->getJson("/api/meal-posts/{$meal->id}")->assertNotFound(); UserBlock::query() ->where('blocker_id', $secondUser->id) ->where('blocked_id', $firstUser->id) ->delete(); $this->getJson("/api/meal-posts/{$meal->id}")->assertOk(); });