'https://example@sentry.test/1']); } public function test_reports_server_errors_to_sentry(): void { $fake = new class { public int $captured = 0; public function captureException(mixed $exception = null): void { $this->captured++; } }; $this->app->instance('sentry', $fake); $handler = $this->app->make(\App\Exceptions\Handler::class); $handler->report(new \RuntimeException('boom')); $this->assertSame(1, $fake->captured, 'Sentry should receive server errors'); } public function test_ignores_client_errors_for_sentry(): void { $fake = new class { public int $captured = 0; public function captureException(mixed $exception = null): void { $this->captured++; } }; $this->app->instance('sentry', $fake); $handler = $this->app->make(\App\Exceptions\Handler::class); $handler->report(new HttpException(404)); $this->assertSame(0, $fake->captured, 'Sentry should ignore 4xx errors'); } public function test_reports_scheduled_task_failures_to_sentry(): void { $fake = new class { public int $captured = 0; public function captureException(mixed $exception = null): void { $this->captured++; } }; $this->app->instance('sentry', $fake); Artisan::call('schedule:list', [ '--json' => true, '--no-interaction' => true, ]); $schedule = $this->app->make(Schedule::class); $task = collect($schedule->events()) ->first(fn ($event) => str_contains((string) $event->command, 'storage:monitor')); $this->assertNotNull($task, 'Expected scheduled storage:monitor task'); $task->finish($this->app, 1); $this->assertSame(1, $fake->captured, 'Sentry should receive scheduled task failures'); } public function test_reports_queue_failures_to_sentry(): void { $fake = new class { public int $captured = 0; public function captureException(mixed $exception = null): void { $this->captured++; } }; $this->app->instance('sentry', $fake); $job = new class implements \Illuminate\Contracts\Queue\Job { public function uuid(): ?string { return null; } public function getJobId(): string { return 'job-id'; } public function payload(): array { return []; } public function fire(): void {} public function release($delay = 0): void {} public function isReleased(): bool { return false; } public function delete(): void {} public function isDeleted(): bool { return false; } public function isDeletedOrReleased(): bool { return false; } public function attempts(): int { return 1; } public function hasFailed(): bool { return false; } public function markAsFailed(): void {} public function fail($e = null): void {} public function maxTries(): ?int { return null; } public function maxExceptions(): ?int { return null; } public function timeout(): ?int { return null; } public function retryUntil(): ?int { return null; } public function getName(): string { return 'FakeJob'; } public function resolveName(): string { return 'FakeJob'; } public function resolveQueuedJobClass(): string { return 'FakeJob'; } public function getConnectionName(): string { return 'redis'; } public function getQueue(): string { return 'default'; } public function getRawBody(): string { return '{}'; } }; Event::dispatch(new JobFailed('redis', $job, new \RuntimeException('Queue failure'))); $this->assertSame(1, $fake->captured, 'Sentry should receive queue failures'); } }