ldc-arcadia-api/tests/Integration/Infra/Jobs/HealthCheckJobTest.php
Jérémy Munsch 62c221f6b6
feat(health): add oracle probe and normalize health checks
Refactor health check DTOs around a common CheckResult contract and remove the\nlegacy HealthChecks aggregate object.\n\nAdd a dedicated /api/v1/health/oracle endpoint with response mapping, OpenAPI\nschema updates, and Bruno request coverage.\n\nAlign feature, unit, integration, architecture and OpenAPI tests with the new\nset-based checks representation and make Oracle health tests deterministic in CI.\n\nUpdate command documentation to the uwil:* command namespace.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
2026-10-06 14:51:46 +02:00

221 lines
8.5 KiB
PHP

<?php
declare(strict_types=1);
namespace Tests\Integration\Infra\Jobs;
use Application\Health\Config\HealthAlertMailConfig;
use Application\Health\Contracts\HealthChecker;
use Application\Health\Dto\CacheCheckResult;
use Application\Health\Dto\DatabaseCheckResult;
use Application\Health\Dto\HealthCheckResult;
use Application\Health\Dto\MailCheckResult;
use Application\Health\Enums\ComponentStatus;
use Application\Health\Enums\HealthStatus;
use Illuminate\Contracts\Mail\Mailer;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Mail;
use Infra\Jobs\HealthCheckJob;
use Infra\Mail\SystemHealthAlertMail;
use Psr\Log\LoggerInterface;
use Shared\Time\Utc;
use Tests\TestCase;
covers(HealthCheckJob::class);
uses(TestCase::class, RefreshDatabase::class);
describe('HealthCheckJob Integration Test', function (): void {
$tempStateFile = null;
beforeEach(function () use (&$tempStateFile): void {
$tempStateFile = sys_get_temp_dir() . '/health_check_test_' . uniqid() . '.json';
if (File::exists($tempStateFile)) {
File::delete($tempStateFile);
}
Mail::fake();
});
afterEach(function () use (&$tempStateFile): void {
if ($tempStateFile !== null && File::exists($tempStateFile)) {
File::delete($tempStateFile);
}
});
it('does not send email on initial run when healthy and creates state file', function () use (&$tempStateFile): void {
// GIVEN
$job = new HealthCheckJob($tempStateFile);
$healthChecker = mock(HealthChecker::class);
$healthyResult = new HealthCheckResult(
status: HealthStatus::HEALTHY,
timestamp: Utc::now(),
checks: [
'database_default' => new DatabaseCheckResult(ComponentStatus::UP, 'sqlite'),
'database_arcadia' => new DatabaseCheckResult(ComponentStatus::UP, 'arcadia'),
'cache' => new CacheCheckResult(ComponentStatus::UP, 'file'),
'mail_smtp' => new MailCheckResult(ComponentStatus::UP, 'log'),
],
);
$healthChecker->shouldReceive('check')->once()->andReturn($healthyResult);
// WHEN
$job->handle(
$healthChecker,
app(Mailer::class),
app(HealthAlertMailConfig::class),
app(LoggerInterface::class),
);
// THEN
expect(File::exists($tempStateFile))->toBeTrue();
$saved = json_decode((string) File::get($tempStateFile), true);
expect($saved['status'])->toBe('healthy');
Mail::assertNothingSent();
});
it('sends degraded alert email on initial run when unhealthy', function () use (&$tempStateFile): void {
// GIVEN
$job = new HealthCheckJob($tempStateFile);
$healthChecker = mock(HealthChecker::class);
$unhealthyResult = new HealthCheckResult(
status: HealthStatus::UNHEALTHY,
timestamp: Utc::now(),
checks: [
'database_default' => new DatabaseCheckResult(ComponentStatus::DOWN, 'sqlite', 'Connection refused'),
'database_arcadia' => new DatabaseCheckResult(ComponentStatus::UP, 'arcadia'),
'cache' => new CacheCheckResult(ComponentStatus::UP, 'file'),
'mail_smtp' => new MailCheckResult(ComponentStatus::UP, 'log'),
],
);
$healthChecker->shouldReceive('check')->once()->andReturn($unhealthyResult);
// WHEN
$job->handle(
$healthChecker,
app(Mailer::class),
app(HealthAlertMailConfig::class),
app(LoggerInterface::class),
);
// THEN
expect(File::exists($tempStateFile))->toBeTrue();
$saved = json_decode((string) File::get($tempStateFile), true);
expect($saved['status'])->toBe('unhealthy');
Mail::assertSent(SystemHealthAlertMail::class, static function (SystemHealthAlertMail $mail): bool {
return $mail->status === HealthStatus::UNHEALTHY
&& $mail->previousStatus === null;
});
});
it('sends alert when status transitions from healthy to unhealthy', function () use (&$tempStateFile): void {
// GIVEN: existing healthy state
File::put($tempStateFile, (string) json_encode([
'status' => 'healthy',
'timestamp' => Utc::now()->format(\DateTimeInterface::RFC3339),
]));
$job = new HealthCheckJob($tempStateFile);
$healthChecker = mock(HealthChecker::class);
$unhealthyResult = new HealthCheckResult(
status: HealthStatus::UNHEALTHY,
timestamp: Utc::now(),
checks: [
'database_default' => new DatabaseCheckResult(ComponentStatus::UP, 'sqlite'),
'database_arcadia' => new DatabaseCheckResult(ComponentStatus::DOWN, 'arcadia', 'DB down'),
'cache' => new CacheCheckResult(ComponentStatus::UP, 'file'),
'mail_smtp' => new MailCheckResult(ComponentStatus::UP, 'log'),
],
);
$healthChecker->shouldReceive('check')->once()->andReturn($unhealthyResult);
// WHEN
$job->handle(
$healthChecker,
app(Mailer::class),
app(HealthAlertMailConfig::class),
app(LoggerInterface::class),
);
// THEN
Mail::assertSent(SystemHealthAlertMail::class, static function (SystemHealthAlertMail $mail): bool {
return $mail->status === HealthStatus::UNHEALTHY
&& $mail->previousStatus === HealthStatus::HEALTHY;
});
});
it('sends recovery alert when status transitions from unhealthy to healthy', function () use (&$tempStateFile): void {
// GIVEN: existing unhealthy state
File::put($tempStateFile, (string) json_encode([
'status' => 'unhealthy',
'timestamp' => Utc::now()->format(\DateTimeInterface::RFC3339),
]));
$job = new HealthCheckJob($tempStateFile);
$healthChecker = mock(HealthChecker::class);
$healthyResult = new HealthCheckResult(
status: HealthStatus::HEALTHY,
timestamp: Utc::now(),
checks: [
'database_default' => new DatabaseCheckResult(ComponentStatus::UP, 'sqlite'),
'database_arcadia' => new DatabaseCheckResult(ComponentStatus::UP, 'arcadia'),
'cache' => new CacheCheckResult(ComponentStatus::UP, 'file'),
'mail_smtp' => new MailCheckResult(ComponentStatus::UP, 'log'),
],
);
$healthChecker->shouldReceive('check')->once()->andReturn($healthyResult);
// WHEN
$job->handle(
$healthChecker,
app(Mailer::class),
app(HealthAlertMailConfig::class),
app(LoggerInterface::class),
);
// THEN
Mail::assertSent(SystemHealthAlertMail::class, static function (SystemHealthAlertMail $mail): bool {
return $mail->status === HealthStatus::HEALTHY
&& $mail->previousStatus === HealthStatus::UNHEALTHY;
});
});
it('does not send email when status remains unchanged', function () use (&$tempStateFile): void {
// GIVEN: existing healthy state
File::put($tempStateFile, (string) json_encode([
'status' => 'healthy',
'timestamp' => Utc::now()->format(\DateTimeInterface::RFC3339),
]));
$job = new HealthCheckJob($tempStateFile);
$healthChecker = mock(HealthChecker::class);
$healthyResult = new HealthCheckResult(
status: HealthStatus::HEALTHY,
timestamp: Utc::now(),
checks: [
'database_default' => new DatabaseCheckResult(ComponentStatus::UP, 'sqlite'),
'database_arcadia' => new DatabaseCheckResult(ComponentStatus::UP, 'arcadia'),
'cache' => new CacheCheckResult(ComponentStatus::UP, 'file'),
'mail_smtp' => new MailCheckResult(ComponentStatus::UP, 'log'),
],
);
$healthChecker->shouldReceive('check')->once()->andReturn($healthyResult);
// WHEN
$job->handle(
$healthChecker,
app(Mailer::class),
app(HealthAlertMailConfig::class),
app(LoggerInterface::class),
);
// THEN
Mail::assertNothingSent();
});
});