create(); $mealPost = MealPosts::factory()->create(); Sanctum::actingAs($reporter); $this->postJson("/api/meal-posts/{$mealPost->id}/reports", [ 'reason' => ReportReason::SPAM->value, 'details' => 'Même image publiée en boucle.', ]) ->assertCreated() ->assertJsonPath('message', __('api.reports.created')) ->assertJsonPath('data.reason', ReportReason::SPAM->value) ->assertJsonPath('data.reportableType', 'meal_post') ->assertJsonPath('data.reportableId', $mealPost->id); $this->assertDatabaseHas('reports', [ 'reporter_id' => $reporter->id, 'reportable_type' => MealPosts::class, 'reportable_id' => $mealPost->id, 'reason' => ReportReason::SPAM->value, 'details' => 'Même image publiée en boucle.', ]); $this->assertDatabaseHas('moderation_cases', [ 'caseable_type' => MealPosts::class, 'caseable_id' => $mealPost->id, 'reports_count' => 1, 'threshold' => 3, ]); }); it('prevents duplicate reports and reports on own content', function () { $owner = User::factory()->create(); $mealPost = MealPosts::factory()->for($owner, 'user')->create(); $reporter = User::factory()->create(); Sanctum::actingAs($reporter); $payload = ['reason' => ReportReason::OTHER->value]; $this->postJson("/api/meal-posts/{$mealPost->id}/reports", $payload) ->assertCreated(); $this->postJson("/api/meal-posts/{$mealPost->id}/reports", $payload) ->assertConflict() ->assertJsonPath('message', __('api.reports.already_exists')); expect(Report::query()->count())->toBe(1); Sanctum::actingAs($owner); $this->postJson("/api/meal-posts/{$mealPost->id}/reports", $payload) ->assertUnprocessable() ->assertJsonPath('message', __('api.reports.cannot_report_own_meal')); }); it('reports users and prevents reporting own profile', function () { $target = User::factory()->create(); $reporter = User::factory()->create(); Sanctum::actingAs($reporter); $this->postJson("/api/users/{$target->id}/reports", [ 'reason' => ReportReason::HARASSMENT->value, ]) ->assertCreated() ->assertJsonPath('data.reportableType', 'user') ->assertJsonPath('data.reportableId', $target->id); $this->assertDatabaseHas('moderation_cases', [ 'caseable_type' => User::class, 'caseable_id' => $target->id, 'threshold' => 5, ]); Sanctum::actingAs($target); $this->postJson("/api/users/{$target->id}/reports", [ 'reason' => ReportReason::OTHER->value, ]) ->assertUnprocessable() ->assertJsonPath('message', __('api.reports.cannot_report_self')); }); it('notifies moderators once when a report threshold is reached', function () { $admin = User::factory()->create(['role' => UserRole::ADMIN]); $moderator = User::factory()->create(['role' => UserRole::MODERATOR]); $regularUser = User::factory()->create(); $mealPost = MealPosts::factory()->create(); foreach (User::factory()->count(3)->create() as $reporter) { Sanctum::actingAs($reporter); $this->postJson("/api/meal-posts/{$mealPost->id}/reports", [ 'reason' => ReportReason::INAPPROPRIATE->value, ])->assertCreated(); } $moderationCase = ModerationCase::query()->where('caseable_id', $mealPost->id)->firstOrFail(); expect($moderationCase->reports_count)->toBe(3) ->and($moderationCase->threshold_notified_at)->not->toBeNull(); expect($admin->notifications)->toHaveCount(1) ->and($admin->notifications->first()->data['format'])->toBe('filament') ->and($admin->notifications->first()->data['title'])->toBe(__('api.notifications.moderation_threshold_title')) ->and($moderator->notifications)->toHaveCount(1) ->and($moderator->notifications->first()->data['format'])->toBe('filament') ->and($regularUser->notifications)->toHaveCount(0); Sanctum::actingAs(User::factory()->create()); $this->postJson("/api/meal-posts/{$mealPost->id}/reports", [ 'reason' => ReportReason::MISLEADING->value, ])->assertCreated(); expect($admin->fresh()->notifications)->toHaveCount(1) ->and($moderator->fresh()->notifications)->toHaveCount(1); });