set('monitoring.alerts.enabled', true); config()->set('monitoring.alerts.mail_to', 'ops@example.com'); Mail::fake(); $monitoringLog = Mockery::spy(LoggerInterface::class); Log::shouldReceive('channel')->with('monitoring')->andReturn($monitoringLog); OperationalAlert::send('Geplanter Task fehlgeschlagen: billing:generate-manual-invoices', ['exit_code' => 1]); $monitoringLog->shouldHaveReceived('critical')->once(); Mail::assertSent(OperationalAlertMail::class, function (OperationalAlertMail $mail) { return $mail->hasTo('ops@example.com') && str_contains($mail->summary, 'billing:generate-manual-invoices') && $mail->context === ['exit_code' => 1]; }); }); test('no mail is sent when alerts are disabled', function () { config()->set('monitoring.alerts.enabled', false); config()->set('monitoring.alerts.mail_to', 'ops@example.com'); Mail::fake(); OperationalAlert::send('Etwas ist kaputt'); Mail::assertNothingSent(); }); test('no mail is sent when no recipient is configured', function () { config()->set('monitoring.alerts.enabled', true); config()->set('monitoring.alerts.mail_to', null); Mail::fake(); OperationalAlert::send('Etwas ist kaputt'); Mail::assertNothingSent(); }); test('a mail failure never bubbles up to the caller', function () { config()->set('monitoring.alerts.enabled', true); config()->set('monitoring.alerts.mail_to', 'ops@example.com'); // Mailserver down: der Versand wirft — der Alert muss das schlucken, // damit der auslösende Scheduler-/Queue-Prozess weiterläuft. Mail::shouldReceive('to')->andReturnSelf(); Mail::shouldReceive('send')->andThrow(new RuntimeException('mail down')); OperationalAlert::send('Etwas ist kaputt'); expect(true)->toBeTrue(); }); test('a failing queue job triggers an operational alert via the registered listener', function () { config()->set('monitoring.alerts.enabled', true); config()->set('monitoring.alerts.mail_to', 'ops@example.com'); Mail::fake(); $job = Mockery::mock(Job::class); $job->shouldReceive('resolveName')->andReturn('App\\Jobs\\ClassifyPressRelease'); $job->shouldReceive('getQueue')->andReturn('classification'); event(new JobFailed('database', $job, new RuntimeException('job boom'))); Mail::assertSent(OperationalAlertMail::class, function (OperationalAlertMail $mail) { return str_contains($mail->summary, 'ClassifyPressRelease'); }); });