diff --git a/src/Application/Health/Dto/HealthCheckResult.php b/src/Application/Health/Dto/HealthCheckResult.php index c19189f..84379ac 100644 --- a/src/Application/Health/Dto/HealthCheckResult.php +++ b/src/Application/Health/Dto/HealthCheckResult.php @@ -5,7 +5,6 @@ declare(strict_types=1); namespace Application\Health\Dto; use Application\Health\Enums\HealthStatus; -use Shared\Time\Utc; final readonly class HealthCheckResult { @@ -20,62 +19,4 @@ final readonly class HealthCheckResult { return $this->status === HealthStatus::HEALTHY; } - - /** - * @return array{ - * status: 'healthy'|'unhealthy', - * timestamp: string, - * checks: array{ - * database_default: array{status: 'up'|'down', connection: string, message?: string|null}, - * database_arcadia: array{status: 'up'|'down', connection: string, message?: string|null}, - * cache: array{status: 'up'|'down', store: string, message?: string|null}, - * mail_smtp: array{status: 'up'|'down', transport: string, message?: string|null} - * } - * } - */ - public function toArray(): array - { - $defaultDb = [ - 'status' => $this->checks->databaseDefault->status->value, - 'connection' => $this->checks->databaseDefault->connection, - ]; - if ($this->checks->databaseDefault->message !== null) { - $defaultDb['message'] = $this->checks->databaseDefault->message; - } - - $arcadiaDb = [ - 'status' => $this->checks->databaseArcadia->status->value, - 'connection' => $this->checks->databaseArcadia->connection, - ]; - if ($this->checks->databaseArcadia->message !== null) { - $arcadiaDb['message'] = $this->checks->databaseArcadia->message; - } - - $cache = [ - 'status' => $this->checks->cache->status->value, - 'store' => $this->checks->cache->store, - ]; - if ($this->checks->cache->message !== null) { - $cache['message'] = $this->checks->cache->message; - } - - $mail = [ - 'status' => $this->checks->mailSmtp->status->value, - 'transport' => $this->checks->mailSmtp->transport, - ]; - if ($this->checks->mailSmtp->message !== null) { - $mail['message'] = $this->checks->mailSmtp->message; - } - - return [ - 'status' => $this->status->value, - 'timestamp' => Utc::toRfc3339($this->timestamp), - 'checks' => [ - 'database_default' => $defaultDb, - 'database_arcadia' => $arcadiaDb, - 'cache' => $cache, - 'mail_smtp' => $mail, - ], - ]; - } } diff --git a/src/Infra/Jobs/HealthCheckJob.php b/src/Infra/Jobs/HealthCheckJob.php index 70d9366..ccb1ab3 100644 --- a/src/Infra/Jobs/HealthCheckJob.php +++ b/src/Infra/Jobs/HealthCheckJob.php @@ -74,7 +74,7 @@ final class HealthCheckJob implements ShouldQueue LoggerInterface $logger, ): void { $failedComponents = []; - $checksArray = $result->toArray()['checks']; + $checksArray = $this->formatChecks($result); foreach ($checksArray as $component => $data) { if ($data['status'] === 'down') { @@ -109,7 +109,7 @@ final class HealthCheckJob implements ShouldQueue HealthAlertMailConfig $mailConfig, LoggerInterface $logger, ): void { - $checksArray = $result->toArray()['checks']; + $checksArray = $this->formatChecks($result); $logger->info('System health check recovered: all components are healthy.', [ 'status' => $result->status->value, @@ -158,9 +158,59 @@ final class HealthCheckJob implements ShouldQueue $payload = [ 'status' => $result->status->value, 'timestamp' => Utc::toRfc3339($result->timestamp), - 'checks' => $result->toArray()['checks'], + 'checks' => $this->formatChecks($result), ]; File::put($filePath, (string) json_encode($payload, \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES)); } + + /** + * @return array{ + * database_default: array{status: string, connection: string, message?: string|null}, + * database_arcadia: array{status: string, connection: string, message?: string|null}, + * cache: array{status: string, store: string, message?: string|null}, + * mail_smtp: array{status: string, transport: string, message?: string|null} + * } + */ + private function formatChecks(HealthCheckResult $result): array + { + $defaultDb = [ + 'status' => $result->checks->databaseDefault->status->value, + 'connection' => $result->checks->databaseDefault->connection, + ]; + if ($result->checks->databaseDefault->message !== null) { + $defaultDb['message'] = $result->checks->databaseDefault->message; + } + + $arcadiaDb = [ + 'status' => $result->checks->databaseArcadia->status->value, + 'connection' => $result->checks->databaseArcadia->connection, + ]; + if ($result->checks->databaseArcadia->message !== null) { + $arcadiaDb['message'] = $result->checks->databaseArcadia->message; + } + + $cache = [ + 'status' => $result->checks->cache->status->value, + 'store' => $result->checks->cache->store, + ]; + if ($result->checks->cache->message !== null) { + $cache['message'] = $result->checks->cache->message; + } + + $mail = [ + 'status' => $result->checks->mailSmtp->status->value, + 'transport' => $result->checks->mailSmtp->transport, + ]; + if ($result->checks->mailSmtp->message !== null) { + $mail['message'] = $result->checks->mailSmtp->message; + } + + return [ + 'database_default' => $defaultDb, + 'database_arcadia' => $arcadiaDb, + 'cache' => $cache, + 'mail_smtp' => $mail, + ]; + } } diff --git a/src/Presentation/Api/V1/Http/Controllers/Health/HealthCheckController.php b/src/Presentation/Api/V1/Http/Controllers/Health/HealthCheckController.php index cf5c6fe..30711ae 100644 --- a/src/Presentation/Api/V1/Http/Controllers/Health/HealthCheckController.php +++ b/src/Presentation/Api/V1/Http/Controllers/Health/HealthCheckController.php @@ -6,6 +6,7 @@ namespace Presentation\Api\V1\Http\Controllers\Health; use Application\Health\Contracts\HealthChecker; use Illuminate\Http\JsonResponse; +use Presentation\Api\V1\Http\Responses\Health\HealthCheckResponse; final readonly class HealthCheckController { @@ -18,9 +19,6 @@ final readonly class HealthCheckController { $result = $this->healthChecker->check(); - return new JsonResponse( - data: $result->toArray(), - status: $result->isHealthy() ? 200 : 503, - ); + return (new HealthCheckResponse($result))->present(); } } diff --git a/src/Presentation/Api/V1/Http/Responses/Health/HealthCheckResponse.php b/src/Presentation/Api/V1/Http/Responses/Health/HealthCheckResponse.php new file mode 100644 index 0000000..e2fa41f --- /dev/null +++ b/src/Presentation/Api/V1/Http/Responses/Health/HealthCheckResponse.php @@ -0,0 +1,91 @@ +present(); + } + + #[\Override] + public function present(): JsonResponse + { + return new JsonResponse( + data: $this->toArray(), + status: $this->result->isHealthy() ? 200 : 503, + ); + } + + /** + * @return array{ + * status: 'healthy'|'unhealthy', + * timestamp: string, + * checks: array{ + * database_default: array{status: 'up'|'down', connection: string, message?: string|null}, + * database_arcadia: array{status: 'up'|'down', connection: string, message?: string|null}, + * cache: array{status: 'up'|'down', store: string, message?: string|null}, + * mail_smtp: array{status: 'up'|'down', transport: string, message?: string|null} + * } + * } + */ + public function toArray(): array + { + $defaultDb = [ + 'status' => $this->result->checks->databaseDefault->status->value, + 'connection' => $this->result->checks->databaseDefault->connection, + ]; + if ($this->result->checks->databaseDefault->message !== null) { + $defaultDb['message'] = $this->result->checks->databaseDefault->message; + } + + $arcadiaDb = [ + 'status' => $this->result->checks->databaseArcadia->status->value, + 'connection' => $this->result->checks->databaseArcadia->connection, + ]; + if ($this->result->checks->databaseArcadia->message !== null) { + $arcadiaDb['message'] = $this->result->checks->databaseArcadia->message; + } + + $cache = [ + 'status' => $this->result->checks->cache->status->value, + 'store' => $this->result->checks->cache->store, + ]; + if ($this->result->checks->cache->message !== null) { + $cache['message'] = $this->result->checks->cache->message; + } + + $mail = [ + 'status' => $this->result->checks->mailSmtp->status->value, + 'transport' => $this->result->checks->mailSmtp->transport, + ]; + if ($this->result->checks->mailSmtp->message !== null) { + $mail['message'] = $this->result->checks->mailSmtp->message; + } + + return [ + 'status' => $this->result->status->value, + 'timestamp' => Utc::toRfc3339($this->result->timestamp), + 'checks' => [ + 'database_default' => $defaultDb, + 'database_arcadia' => $arcadiaDb, + 'cache' => $cache, + 'mail_smtp' => $mail, + ], + ]; + } +} diff --git a/src/Presentation/Console/Commands/HealthCheckCommand.php b/src/Presentation/Console/Commands/HealthCheckCommand.php index 8d74ba8..b974aac 100644 --- a/src/Presentation/Console/Commands/HealthCheckCommand.php +++ b/src/Presentation/Console/Commands/HealthCheckCommand.php @@ -7,6 +7,7 @@ namespace Presentation\Console\Commands; use Application\Health\Contracts\HealthChecker; use Application\Health\Dto\HealthCheckResult; use Illuminate\Console\Command; +use Presentation\Api\V1\Http\Responses\Health\HealthCheckResponse; final class HealthCheckCommand extends Command { @@ -33,7 +34,7 @@ final class HealthCheckCommand extends Command if ($this->option('json')) { $this->line((string) json_encode( - $result->toArray(), + (new HealthCheckResponse($result))->toArray(), \JSON_PRETTY_PRINT | \JSON_UNESCAPED_SLASHES )); diff --git a/tests/Feature/Console/Commands/HealthCheckCommandTest.php b/tests/Feature/Console/Commands/HealthCheckCommandTest.php index 1286b09..007c34b 100644 --- a/tests/Feature/Console/Commands/HealthCheckCommandTest.php +++ b/tests/Feature/Console/Commands/HealthCheckCommandTest.php @@ -63,10 +63,10 @@ describe('HealthCheckCommand', function (): void { ->assertSuccessful(); }); - it('outputs JSON when --json option is passed and fails when unhealthy', function () use (&$originalArcadiaDb): void { + it('outputs JSON when --json option is passed and fails when unhealthy', function () use (&$originalDbDefault): void { // GIVEN /** @var \Tests\TestCase $this */ - Config::set('database.arcadia_meta_model', 'invalid_arcadia_connection'); + Config::set('database.default', 'invalid_connection'); // WHEN $this->artisan('health:check', ['--json' => true]) diff --git a/tests/Unit/Application/Health/HealthCheckDtoTest.php b/tests/Unit/Application/Health/HealthCheckDtoTest.php index 495f628..fa16740 100644 --- a/tests/Unit/Application/Health/HealthCheckDtoTest.php +++ b/tests/Unit/Application/Health/HealthCheckDtoTest.php @@ -100,7 +100,7 @@ describe('Health Check DTOs & Enums Unit Tests', function (): void { ->and($checksDown->isAllUp())->toBeFalse(); }); - it('centralizes complete mapping in HealthCheckResult', function (): void { + it('instantiates HealthCheckResult and evaluates isHealthy', function (): void { // GIVEN $date = new \DateTimeImmutable('2026-08-21 14:30:00 UTC'); $checks = new HealthChecks( @@ -116,29 +116,10 @@ describe('Health Check DTOs & Enums Unit Tests', function (): void { checks: $checks, ); - // WHEN - $array = $result->toArray(); - - // THEN + // WHEN & THEN expect($result->isHealthy())->toBeFalse() - ->and($array['status'])->toBe('unhealthy') - ->and($array['timestamp'])->toBe('2026-08-21T14:30:00+00:00') - ->and($array['checks']['database_default'])->toBe([ - 'status' => 'up', - 'connection' => 'sqlite', - ]) - ->and($array['checks']['database_arcadia'])->toBe([ - 'status' => 'down', - 'connection' => 'arcadia', - 'message' => 'Timeout connecting to DB', - ]) - ->and($array['checks']['cache'])->toBe([ - 'status' => 'up', - 'store' => 'database', - ]) - ->and($array['checks']['mail_smtp'])->toBe([ - 'status' => 'up', - 'transport' => 'smtp', - ]); + ->and($result->status)->toBe(HealthStatus::UNHEALTHY) + ->and($result->timestamp)->toBe($date) + ->and($result->checks->databaseArcadia->message)->toBe('Timeout connecting to DB'); }); }); diff --git a/tests/Unit/Presentation/Api/V1/Http/Responses/Health/HealthCheckResponseTest.php b/tests/Unit/Presentation/Api/V1/Http/Responses/Health/HealthCheckResponseTest.php new file mode 100644 index 0000000..9fe90c9 --- /dev/null +++ b/tests/Unit/Presentation/Api/V1/Http/Responses/Health/HealthCheckResponseTest.php @@ -0,0 +1,66 @@ +toArray(); + $json = $response->present(); + + // THEN + expect($json->getStatusCode())->toBe(200) + ->and($array['status'])->toBe('healthy') + ->and($array['timestamp'])->toBe('2026-08-27T15:00:00+00:00') + ->and($array['checks']['database_default']['status'])->toBe('up') + ->and($array['checks']['database_arcadia']['status'])->toBe('up'); + }); + + it('serializes unhealthy HealthCheckResult into 503 JsonResponse and array', function (): void { + // GIVEN + $date = new \DateTimeImmutable('2026-08-27 15:00:00 UTC'); + $checks = new HealthChecks( + databaseDefault: new DatabaseCheckResult(ComponentStatus::DOWN, 'sqlite', 'Connection refused'), + databaseArcadia: new DatabaseCheckResult(ComponentStatus::UP, 'arcadia'), + cache: new CacheCheckResult(ComponentStatus::UP, 'file'), + mailSmtp: new MailCheckResult(ComponentStatus::UP, 'log'), + ); + + $result = new HealthCheckResult(HealthStatus::UNHEALTHY, $date, $checks); + $response = new HealthCheckResponse($result); + + // WHEN + $array = $response->toArray(); + $json = $response->present(); + + // THEN + expect($json->getStatusCode())->toBe(503) + ->and($array['status'])->toBe('unhealthy') + ->and($array['checks']['database_default']['message'])->toBe('Connection refused'); + }); +});