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>
221 lines
8.5 KiB
PHP
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();
|
|
});
|
|
});
|