diff --git a/README.md b/README.md index 9eec002..37dbc2c 100644 --- a/README.md +++ b/README.md @@ -50,27 +50,27 @@ vendor/bin/phpcbf ```bash # Vérifier l'état de l'application -php artisan health +php artisan uwil:health # List all Arcadia brands -php artisan arcadia:brand:list +php artisan uwil:arcadia:brand:list # List all Arcadia clients -php artisan arcadia:client:list +php artisan uwil:arcadia:client:list # Create a new Arcadia client (interactive wizard) -php artisan arcadia:client:create +php artisan uwil:arcadia:client:create # Regenerate the secret of an API client -php artisan arcadia:client:regenerate-secret +php artisan uwil:arcadia:client:regenerate-secret # Toggle the status of an API client -php artisan arcadia:client:toggle-status +php artisan uwil:arcadia:client:toggle-status # Check schema and query conformity of the replicated Arcadia Oracle database -php artisan arcadia:database:check-conformity +php artisan uwil:arcadia:database:check-conformity # Display or export security audit events since a specified date -php artisan arcadia:security:audit-logs +php artisan uwil:arcadia:security:audit-logs ``` diff --git a/documentation/Arcadia/README.md b/documentation/Arcadia/README.md index fc73747..ea11fbc 100644 --- a/documentation/Arcadia/README.md +++ b/documentation/Arcadia/README.md @@ -159,6 +159,6 @@ curl -X GET "https://api.arcadia.ldc.fr/api/v1/brands" \ #### Commande CLI Artisan ```bash -php artisan arcadia:brand:list +php artisan uwil:arcadia:brand:list ``` diff --git a/documentation/Bruno/Arcadia Api/Api/V1/Oracle Health Check.yml b/documentation/Bruno/Arcadia Api/Api/V1/Oracle Health Check.yml new file mode 100644 index 0000000..b6cbbe0 --- /dev/null +++ b/documentation/Bruno/Arcadia Api/Api/V1/Oracle Health Check.yml @@ -0,0 +1,15 @@ +info: + name: Oracle Health Check + type: http + seq: 5 + +http: + method: GET + url: "{{ENDPOINT}}/api/{{APIV1}}/health/oracle" + auth: inherit + +settings: + encodeUrl: true + timeout: 0 + followRedirects: true + maxRedirects: 5 diff --git a/documentation/OpenApi/openapi.yaml b/documentation/OpenApi/openapi.yaml index c370c61..839cd03 100644 --- a/documentation/OpenApi/openapi.yaml +++ b/documentation/OpenApi/openapi.yaml @@ -60,6 +60,30 @@ paths: schema: $ref: "#/components/schemas/HealthCheckResponse" + /health/oracle: + get: + tags: + - System + summary: Sonde dédiée à la connectivité Oracle + description: | + Vérifie exclusivement la connectivité à la base Oracle Arcadia. + Cette sonde est destinée au monitoring externe (ex: UptimeRobot) et ne teste ni cache ni SMTP. + operationId: checkOracleHealth + security: [] + responses: + "200": + description: Connectivité Oracle opérationnelle. + content: + application/json: + schema: + $ref: "#/components/schemas/OracleHealthCheckResponse" + "503": + description: Connectivité Oracle indisponible. + content: + application/json: + schema: + $ref: "#/components/schemas/OracleHealthCheckResponse" + /employees: get: tags: @@ -681,6 +705,44 @@ components: - "null" description: Message d'erreur éventuel en cas d'échec. + OracleHealthCheckResponse: + type: object + description: Résultat de la sonde de connectivité Oracle. + required: + - status + - timestamp + - oracle + properties: + status: + type: string + enum: + - healthy + - unhealthy + description: État de santé global de la connectivité Oracle. + example: "healthy" + timestamp: + type: string + format: date-time + description: Horodatage de la vérification au format RFC 3339 UTC. + example: "2026-08-21T14:30:00Z" + oracle: + type: object + required: + - status + - connection + properties: + status: + type: string + enum: + - up + - down + description: État de la connectivité Oracle. + example: "up" + connection: + type: string + description: Nom de la connexion Oracle vérifiée. + example: "arcadia" + Brand: type: object description: Représentation canonique d'une marque dans Arcadia API. diff --git a/routes/apiv1.php b/routes/apiv1.php index f756afb..927cb7d 100644 --- a/routes/apiv1.php +++ b/routes/apiv1.php @@ -9,6 +9,7 @@ use Presentation\Api\V1\Http\Controllers\Brands\BrandsIndexController; use Presentation\Api\V1\Http\Controllers\Employees\EmployeeShowController; use Presentation\Api\V1\Http\Controllers\Employees\EmployeesIndexController; use Presentation\Api\V1\Http\Controllers\Health\HealthCheckController; +use Presentation\Api\V1\Http\Controllers\Health\OracleHealthCheckController; use Presentation\Api\V1\Http\Controllers\IncoCompositions\IncoCompositionShowController; use Presentation\Api\V1\Http\Controllers\IncoSheets\IncoSheetsIndexController; use Presentation\Api\V1\Http\Controllers\IncoSheets\IncoSheetsSearchController; @@ -32,6 +33,7 @@ Route::get('/', fn () => response()->json([ // * HEALTH CHECK Route::get('/health', HealthCheckController::class)->name('health'); +Route::get('/health/oracle', OracleHealthCheckController::class)->name('health.oracle'); // * OPENAPI Route::get('/openapi', function () { diff --git a/src/Application/Health/Contracts/HealthChecker.php b/src/Application/Health/Contracts/HealthChecker.php index 6b2015e..aacb098 100644 --- a/src/Application/Health/Contracts/HealthChecker.php +++ b/src/Application/Health/Contracts/HealthChecker.php @@ -11,5 +11,7 @@ interface HealthChecker { public function check(): HealthCheckResult; + public function checkArcadiaConnectivity(): HealthCheckResult; + public function checkArcadiaConformity(): ArcadiaConformityResult; } diff --git a/src/Application/Health/Dto/CacheCheckResult.php b/src/Application/Health/Dto/CacheCheckResult.php index 313ea29..de93f92 100644 --- a/src/Application/Health/Dto/CacheCheckResult.php +++ b/src/Application/Health/Dto/CacheCheckResult.php @@ -6,7 +6,7 @@ namespace Application\Health\Dto; use Application\Health\Enums\ComponentStatus; -final readonly class CacheCheckResult +final readonly class CacheCheckResult implements CheckResult { public function __construct( public ComponentStatus $status, @@ -14,4 +14,14 @@ final readonly class CacheCheckResult public ?string $message = null, ) { } + + public function getName(): string + { + return $this->store; + } + + public function getStatus(): ComponentStatus + { + return $this->status; + } } diff --git a/src/Application/Health/Dto/CheckResult.php b/src/Application/Health/Dto/CheckResult.php new file mode 100644 index 0000000..aa7b004 --- /dev/null +++ b/src/Application/Health/Dto/CheckResult.php @@ -0,0 +1,14 @@ +connection; + } + + public function getStatus(): ComponentStatus + { + return $this->status; + } } diff --git a/src/Application/Health/Dto/HealthCheckResult.php b/src/Application/Health/Dto/HealthCheckResult.php index 84379ac..a8a6c32 100644 --- a/src/Application/Health/Dto/HealthCheckResult.php +++ b/src/Application/Health/Dto/HealthCheckResult.php @@ -8,10 +8,11 @@ use Application\Health\Enums\HealthStatus; final readonly class HealthCheckResult { + /** @param \Application\Health\Dto\CheckResult[] $checks */ public function __construct( public HealthStatus $status, public \DateTimeImmutable $timestamp, - public HealthChecks $checks, + public array $checks, ) { } diff --git a/src/Application/Health/Dto/HealthChecks.php b/src/Application/Health/Dto/HealthChecks.php deleted file mode 100644 index 8a88bc2..0000000 --- a/src/Application/Health/Dto/HealthChecks.php +++ /dev/null @@ -1,26 +0,0 @@ -databaseDefault->status === ComponentStatus::UP - && $this->databaseArcadia->status === ComponentStatus::UP - && $this->cache->status === ComponentStatus::UP - && $this->mailSmtp->status === ComponentStatus::UP; - } -} diff --git a/src/Application/Health/Dto/MailCheckResult.php b/src/Application/Health/Dto/MailCheckResult.php index e3905bb..0ecda0b 100644 --- a/src/Application/Health/Dto/MailCheckResult.php +++ b/src/Application/Health/Dto/MailCheckResult.php @@ -6,7 +6,7 @@ namespace Application\Health\Dto; use Application\Health\Enums\ComponentStatus; -final readonly class MailCheckResult +final readonly class MailCheckResult implements CheckResult { public function __construct( public ComponentStatus $status, @@ -14,4 +14,14 @@ final readonly class MailCheckResult public ?string $message = null, ) { } + + public function getName(): string + { + return $this->transport; + } + + public function getStatus(): ComponentStatus + { + return $this->status; + } } diff --git a/src/Infra/Jobs/HealthCheckJob.php b/src/Infra/Jobs/HealthCheckJob.php index 2c7f0fd..e8845df 100644 --- a/src/Infra/Jobs/HealthCheckJob.php +++ b/src/Infra/Jobs/HealthCheckJob.php @@ -6,7 +6,11 @@ namespace Infra\Jobs; use Application\Health\Config\HealthAlertMailConfig; use Application\Health\Contracts\HealthChecker; +use Application\Health\Dto\CacheCheckResult; +use Application\Health\Dto\CheckResult; +use Application\Health\Dto\DatabaseCheckResult; use Application\Health\Dto\HealthCheckResult; +use Application\Health\Dto\MailCheckResult; use Application\Health\Enums\HealthStatus; use Application\Health\Exceptions\SystemHealthDegradedException; use Illuminate\Contracts\Mail\Mailer; @@ -64,12 +68,10 @@ final class HealthCheckJob LoggerInterface $logger, ): void { $failedComponents = []; - $checksArray = $this->formatChecks($result); + $checks = []; - foreach ($checksArray as $component => $data) { - if ($data['status'] === 'down') { - $failedComponents[] = sprintf('%s (%s)', $component, $data['message'] ?? 'down'); - } + foreach ($result->checks as $check) { + $checks[$check->getName()] = $this->mapCheckToArray($check); } $reason = implode(', ', $failedComponents); @@ -77,7 +79,7 @@ final class HealthCheckJob 'status' => $result->status->value, 'previousStatus' => $previousStatus?->value, 'timestamp' => Utc::toRfc3339($result->timestamp), - 'checks' => $checksArray, + 'checks' => $checks, ]); report(new SystemHealthDegradedException("System health check degraded: {$reason}")); @@ -85,7 +87,7 @@ final class HealthCheckJob $mailable = new SystemHealthAlertMail( status: $result->status, occurredAt: $result->timestamp, - checks: $checksArray, + checks: $checks, previousStatus: $previousStatus, ); @@ -99,7 +101,11 @@ final class HealthCheckJob HealthAlertMailConfig $mailConfig, LoggerInterface $logger, ): void { - $checksArray = $this->formatChecks($result); + $checks = []; + + foreach ($result->checks as $check) { + $checks[$check->getName()] = $this->mapCheckToArray($check); + } $logger->info('System health check recovered: all components are healthy.', [ 'status' => $result->status->value, @@ -110,7 +116,7 @@ final class HealthCheckJob $mailable = new SystemHealthAlertMail( status: $result->status, occurredAt: $result->timestamp, - checks: $checksArray, + checks: $checks, previousStatus: $previousStatus, ); @@ -145,62 +151,43 @@ final class HealthCheckJob File::makeDirectory($dir, 0755, true); } + $checks = []; + + foreach ($result->checks as $check) { + $checks[$check->getName()] = $this->mapCheckToArray($check); + } + $payload = [ 'status' => $result->status->value, 'timestamp' => Utc::toRfc3339($result->timestamp), - 'checks' => $this->formatChecks($result), + 'checks' => $checks, ]; 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 + /** @return array */ + private function mapCheckToArray(CheckResult $check): 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; - } + $noMessageMapper = fn (string | null $message) => $message ? $message : 'N/A'; - $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, - ]; + return match (true) { + $check instanceof DatabaseCheckResult => [ + 'status' => $check->status->value, + 'connection' => $check->connection, + 'message' => $noMessageMapper($check->message), + ], + $check instanceof CacheCheckResult => [ + 'status' => $check->status->value, + 'store' => $check->store, + 'message' => $noMessageMapper($check->message), + ], + $check instanceof MailCheckResult => [ + 'status' => $check->status->value, + 'transport' => $check->transport, + 'message' => $noMessageMapper($check->message), + ], + default => throw new \InvalidArgumentException('Unsupported check result type'), + }; } } diff --git a/src/Infra/Services/Health/LaravelSystemHealthChecker.php b/src/Infra/Services/Health/LaravelSystemHealthChecker.php index 0594dd3..d602451 100644 --- a/src/Infra/Services/Health/LaravelSystemHealthChecker.php +++ b/src/Infra/Services/Health/LaravelSystemHealthChecker.php @@ -10,7 +10,6 @@ use Application\Health\Dto\CacheCheckResult; use Application\Health\Dto\DatabaseCheckResult; use Application\Health\Dto\DirectoryConformityCheck; use Application\Health\Dto\HealthCheckResult; -use Application\Health\Dto\HealthChecks; use Application\Health\Dto\MailCheckResult; use Application\Health\Enums\ComponentStatus; use Application\Health\Enums\HealthStatus; @@ -21,6 +20,7 @@ use Illuminate\Support\Facades\Config; use Illuminate\Support\Facades\DB; use Infra\Config\ArcadiaStorageConfig; use Shared\Time\Utc; +use Application\Health\Dto\CheckResult; final readonly class LaravelSystemHealthChecker implements HealthChecker { @@ -37,14 +37,24 @@ final readonly class LaravelSystemHealthChecker implements HealthChecker $cache = $this->checkCache(); $mailSmtp = $this->checkMailSmtp(); - $checks = new HealthChecks( - databaseDefault: $databaseDefault, - databaseArcadia: $databaseArcadia, - cache: $cache, - mailSmtp: $mailSmtp, - ); + $checks = [ + $databaseDefault, + $databaseArcadia, + $cache, + $mailSmtp, + ]; - $status = $checks->isAllUp() ? HealthStatus::HEALTHY : HealthStatus::UNHEALTHY; + $status = array_reduce( + $checks, + function (HealthStatus $carry, CheckResult $check) { + if ($carry !== HealthStatus::HEALTHY) { + return $carry; + } + return $check->getStatus() === ComponentStatus::UP ? + HealthStatus::HEALTHY : HealthStatus::UNHEALTHY; + }, + HealthStatus::HEALTHY + ); return new HealthCheckResult( status: $status, @@ -53,6 +63,19 @@ final readonly class LaravelSystemHealthChecker implements HealthChecker ); } + #[\Override] + public function checkArcadiaConnectivity(): HealthCheckResult + { + $databaseArcadia = $this->checkArcadiaDatabase(); + + return new HealthCheckResult( + status: $databaseArcadia->getStatus() === ComponentStatus::UP ? + HealthStatus::HEALTHY : HealthStatus::UNHEALTHY, + timestamp: Utc::now(), + checks: [$databaseArcadia], + ); + } + #[\Override] public function checkArcadiaConformity(): ArcadiaConformityResult { diff --git a/src/Presentation/Api/V1/Http/Controllers/Health/OracleHealthCheckController.php b/src/Presentation/Api/V1/Http/Controllers/Health/OracleHealthCheckController.php new file mode 100644 index 0000000..f538c69 --- /dev/null +++ b/src/Presentation/Api/V1/Http/Controllers/Health/OracleHealthCheckController.php @@ -0,0 +1,45 @@ +healthChecker->checkArcadiaConnectivity(); + $arcadia = null; + + foreach ($result->checks as $check) { + if ($check instanceof DatabaseCheckResult) { + $arcadia = $check; + break; + } + } + + if (! $arcadia instanceof DatabaseCheckResult) { + $arcadia = new DatabaseCheckResult( + status: ComponentStatus::DOWN, + connection: 'arcadia', + message: 'Arcadia connectivity check result is missing.', + ); + } + + return (new OracleHealthCheckResponse( + result: $arcadia, + timestamp: $result->timestamp, + ))->present(); + } +} diff --git a/src/Presentation/Api/V1/Http/Responses/Health/HealthCheckResponse.php b/src/Presentation/Api/V1/Http/Responses/Health/HealthCheckResponse.php index e2fa41f..8eab436 100644 --- a/src/Presentation/Api/V1/Http/Responses/Health/HealthCheckResponse.php +++ b/src/Presentation/Api/V1/Http/Responses/Health/HealthCheckResponse.php @@ -4,7 +4,12 @@ declare(strict_types=1); namespace Presentation\Api\V1\Http\Responses\Health; +use Application\Health\Dto\CacheCheckResult; +use Application\Health\Dto\CheckResult; +use Application\Health\Dto\DatabaseCheckResult; use Application\Health\Dto\HealthCheckResult; +use Application\Health\Dto\MailCheckResult; +use Application\Health\Enums\HealthStatus; use Illuminate\Http\JsonResponse; use Presentation\Api\V1\Contracts\ApiResponse; use Shared\Time\Utc; @@ -31,61 +36,44 @@ final readonly class HealthCheckResponse implements ApiResponse ); } - /** - * @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} - * } - * } - */ + /** @return array{status: 'healthy'|'unhealthy', timestamp: string, checks: array>} */ 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; - } + $checks = []; - $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; + foreach ($this->result->checks as $check) { + $checks[$check->getName()] = $this->mapCheckToArray($check); } return [ - 'status' => $this->result->status->value, + 'status' => $this->result->status === HealthStatus::HEALTHY ? 'healthy' : 'unhealthy', 'timestamp' => Utc::toRfc3339($this->result->timestamp), - 'checks' => [ - 'database_default' => $defaultDb, - 'database_arcadia' => $arcadiaDb, - 'cache' => $cache, - 'mail_smtp' => $mail, - ], + 'checks' => $checks, ]; } + + /** @return array */ + private function mapCheckToArray(CheckResult $check): array + { + $noMessageMapper = fn (string | null $message) => $message ? $message : 'N/A'; + + return match (true) { + $check instanceof DatabaseCheckResult => [ + 'status' => $check->status->value, + 'connection' => $check->connection, + 'message' => $noMessageMapper($check->message), + ], + $check instanceof CacheCheckResult => [ + 'status' => $check->status->value, + 'store' => $check->store, + 'message' => $noMessageMapper($check->message), + ], + $check instanceof MailCheckResult => [ + 'status' => $check->status->value, + 'transport' => $check->transport, + 'message' => $noMessageMapper($check->message), + ], + default => throw new \InvalidArgumentException('Unsupported check result type'), + }; + } } diff --git a/src/Presentation/Api/V1/Http/Responses/Health/OracleHealthCheckResponse.php b/src/Presentation/Api/V1/Http/Responses/Health/OracleHealthCheckResponse.php new file mode 100644 index 0000000..dcba057 --- /dev/null +++ b/src/Presentation/Api/V1/Http/Responses/Health/OracleHealthCheckResponse.php @@ -0,0 +1,57 @@ +present(); + } + + #[\Override] + public function present(): JsonResponse + { + return new JsonResponse( + data: $this->toArray(), + status: $this->result->status === ComponentStatus::UP ? 200 : 503, + ); + } + + /** + * @return array{ + * status: 'healthy'|'unhealthy', + * timestamp: string, + * oracle: array{ + * status: 'up'|'down', + * connection: string + * } + * } + */ + public function toArray(): array + { + return [ + 'status' => $this->result->status === ComponentStatus::UP ? 'healthy' : 'unhealthy', + 'timestamp' => Utc::toRfc3339($this->timestamp), + 'oracle' => [ + 'status' => $this->result->status->value, + 'connection' => $this->result->connection, + ], + ]; + } +} diff --git a/src/Presentation/Console/Commands/HealthCheckCommand.php b/src/Presentation/Console/Commands/HealthCheckCommand.php index 4303201..3690eed 100644 --- a/src/Presentation/Console/Commands/HealthCheckCommand.php +++ b/src/Presentation/Console/Commands/HealthCheckCommand.php @@ -5,7 +5,11 @@ declare(strict_types=1); namespace Presentation\Console\Commands; use Application\Health\Contracts\HealthChecker; +use Application\Health\Dto\CacheCheckResult; +use Application\Health\Dto\CheckResult; +use Application\Health\Dto\DatabaseCheckResult; use Application\Health\Dto\HealthCheckResult; +use Application\Health\Dto\MailCheckResult; use Illuminate\Console\Command; use Presentation\Api\V1\Http\Responses\Health\HealthCheckResponse; @@ -56,33 +60,34 @@ final class HealthCheckCommand extends Command private function renderTable(HealthCheckResult $result): void { - $rows = [ - [ - 'Default Database Connectivity', - $result->checks->databaseDefault->status->value, - $result->checks->databaseDefault->connection . ($result->checks->databaseDefault->message !== null ? - " ({$result->checks->databaseDefault->message})" : ''), - ], - [ - 'Arcadia Database Connectivity', - $result->checks->databaseArcadia->status->value, - $result->checks->databaseArcadia->connection . ($result->checks->databaseArcadia->message !== null ? - " ({$result->checks->databaseArcadia->message})" : ''), - ], - [ - 'Cache Store Connectivity', - $result->checks->cache->status->value, - $result->checks->cache->store . ($result->checks->cache->message !== null ? - " ({$result->checks->cache->message})" : ''), - ], - [ - 'Mail SMTP Connectivity', - $result->checks->mailSmtp->status->value, - $result->checks->mailSmtp->transport . ($result->checks->mailSmtp->message !== null ? - " ({$result->checks->mailSmtp->message})" : ''), - ], - ]; + $rows = []; + foreach ($result->checks as $check) { + $rows[] = $this->mapCheckToArray($check); + } $this->table(['Component', 'Status', 'Details'], $rows); } + + /** @return array{name: string, status: string, message: string} */ + private function mapCheckToArray(CheckResult $check): array + { + return match (true) { + $check instanceof DatabaseCheckResult => [ + 'name' => $check->getName(), + 'status' => $check->status->value, + 'message' => $check->message ?? 'N/A', + ], + $check instanceof CacheCheckResult => [ + 'name' => $check->getName(), + 'status' => $check->status->value, + 'message' => $check->message ?? 'N/A', + ], + $check instanceof MailCheckResult => [ + 'name' => $check->getName(), + 'status' => $check->status->value, + 'message' => $check->message ?? 'N/A', + ], + default => throw new \InvalidArgumentException('Unsupported check result type'), + }; + } } diff --git a/tests/Architecture/ApplicationTest.php b/tests/Architecture/ApplicationTest.php index 13c3321..90871b7 100644 --- a/tests/Architecture/ApplicationTest.php +++ b/tests/Architecture/ApplicationTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Tests\Architecture; +use Application\Health\Dto\CheckResult; use Application\Security\Models\ValueObjects\Request\HttpMethod; use Shared\DDD\Event\Event; use Shared\DDD\Port\Directory; @@ -111,7 +112,10 @@ describe('Application Architecture Rules', function (): void { ->expect('Application\Health\Dto') ->classes() ->toBeReadonly() - ->toBeFinal(); + ->toBeFinal() + ->ignoring([ + CheckResult::class, + ]); arch('Application Config classes are final and readonly') ->expect([ diff --git a/tests/Feature/Api/V1/Health/HealthCheckTest.php b/tests/Feature/Api/V1/Health/HealthCheckTest.php index 95cf56d..5143b38 100644 --- a/tests/Feature/Api/V1/Health/HealthCheckTest.php +++ b/tests/Feature/Api/V1/Health/HealthCheckTest.php @@ -33,6 +33,10 @@ describe('Health Check API (GET /api/v1/health)', function (): void { it('returns 200 healthy when all infrastructure components are operational', function (): void { // GIVEN /** @var TestCase $this */ + $databaseDefault = Config::string('database.default'); + $databaseArcadia = Config::string('database.arcadia_meta_model'); + $cacheStore = Config::string('cache.default'); + $mailTransport = Config::string('mail.default'); // WHEN $response = $this->getJson('/api/v1/health'); @@ -42,28 +46,20 @@ describe('Health Check API (GET /api/v1/health)', function (): void { ->assertJsonStructure([ 'status', 'timestamp', - 'checks' => [ - 'database_default' => ['status', 'connection'], - 'database_arcadia' => ['status', 'connection'], - 'cache' => ['status', 'store'], - 'mail_smtp' => ['status', 'transport'], - ], + 'checks', ]) - ->assertJson([ - 'status' => 'healthy', - 'checks' => [ - 'database_default' => ['status' => 'up'], - 'database_arcadia' => ['status' => 'up'], - 'cache' => ['status' => 'up'], - 'mail_smtp' => ['status' => 'up'], - ], - ]); + ->assertJsonPath('status', 'healthy') + ->assertJsonPath('checks.' . $databaseDefault . '.status', 'up') + ->assertJsonPath('checks.' . $databaseArcadia . '.status', 'up') + ->assertJsonPath('checks.' . $cacheStore . '.status', 'up') + ->assertJsonPath('checks.' . $mailTransport . '.status', 'up'); }); it('returns 503 unhealthy when default database connection fails', function () use (&$originalDbDefault): void { // GIVEN /** @var TestCase $this */ Config::set('database.default', 'invalid_connection'); + $databaseDefault = Config::string('database.default'); try { // WHEN @@ -71,12 +67,8 @@ describe('Health Check API (GET /api/v1/health)', function (): void { // THEN $response->assertStatus(503) - ->assertJson([ - 'status' => 'unhealthy', - 'checks' => [ - 'database_default' => ['status' => 'down'], - ], - ]); + ->assertJsonPath('status', 'unhealthy') + ->assertJsonPath('checks.' . $databaseDefault . '.status', 'down'); } finally { Config::set('database.default', $originalDbDefault); } @@ -92,6 +84,7 @@ describe('Health Check API (GET /api/v1/health)', function (): void { 'database' => 'invalid_arcadia_db', ]); Config::set('database.arcadia_meta_model', 'invalid_arcadia_connection'); + $databaseArcadia = Config::string('database.arcadia_meta_model'); try { // WHEN @@ -99,12 +92,8 @@ describe('Health Check API (GET /api/v1/health)', function (): void { // THEN $response->assertStatus(503) - ->assertJson([ - 'status' => 'unhealthy', - 'checks' => [ - 'database_arcadia' => ['status' => 'down'], - ], - ]); + ->assertJsonPath('status', 'unhealthy') + ->assertJsonPath('checks.' . $databaseArcadia . '.status', 'down'); } finally { Config::set('database.arcadia_meta_model', $originalArcadiaDb); } @@ -114,6 +103,7 @@ describe('Health Check API (GET /api/v1/health)', function (): void { // GIVEN /** @var TestCase $this */ Config::set('cache.default', 'invalid_cache_store'); + $cacheStore = Config::string('cache.default'); try { // WHEN @@ -121,12 +111,8 @@ describe('Health Check API (GET /api/v1/health)', function (): void { // THEN $response->assertStatus(503) - ->assertJson([ - 'status' => 'unhealthy', - 'checks' => [ - 'cache' => ['status' => 'down'], - ], - ]); + ->assertJsonPath('status', 'unhealthy') + ->assertJsonPath('checks.' . $cacheStore . '.status', 'down'); } finally { Config::set('cache.default', $originalCache); } @@ -142,6 +128,7 @@ describe('Health Check API (GET /api/v1/health)', function (): void { 'port' => 65432, // Non-existent port 'timeout' => 0.1, ]); + $mailTransport = Config::string('mail.default'); try { // WHEN @@ -149,12 +136,8 @@ describe('Health Check API (GET /api/v1/health)', function (): void { // THEN $response->assertStatus(503) - ->assertJson([ - 'status' => 'unhealthy', - 'checks' => [ - 'mail_smtp' => ['status' => 'down'], - ], - ]); + ->assertJsonPath('status', 'unhealthy') + ->assertJsonPath('checks.' . $mailTransport . '.status', 'down'); } finally { Config::set('mail.default', $originalMail); Config::set('mail.mailers.smtp', $originalMailer); diff --git a/tests/Feature/Api/V1/Health/OracleHealthCheckTest.php b/tests/Feature/Api/V1/Health/OracleHealthCheckTest.php new file mode 100644 index 0000000..70eefe7 --- /dev/null +++ b/tests/Feature/Api/V1/Health/OracleHealthCheckTest.php @@ -0,0 +1,67 @@ +getJson('/api/v1/health/oracle'); + + // THEN + $response->assertStatus(200) + ->assertJsonStructure([ + 'status', + 'timestamp', + 'oracle' => ['status', 'connection'], + ]) + ->assertJson([ + 'status' => 'healthy', + 'oracle' => ['status' => 'up'], + ]); + }); + + it('returns 503 unhealthy when oracle connectivity fails', function () use (&$originalArcadiaDb): void { + // GIVEN + /** @var TestCase $this */ + Config::set('database.connections.invalid_arcadia_connection', [ + 'driver' => 'mysql', + 'host' => '127.0.0.1', + 'port' => 65433, + 'database' => 'invalid_arcadia_db', + ]); + Config::set('database.arcadia_meta_model', 'invalid_arcadia_connection'); + + try { + // WHEN + $response = $this->getJson('/api/v1/health/oracle'); + + // THEN + $response->assertStatus(503) + ->assertJson([ + 'status' => 'unhealthy', + 'oracle' => ['status' => 'down'], + ]); + } finally { + Config::set('database.arcadia_meta_model', $originalArcadiaDb); + } + }); +}); diff --git a/tests/Feature/Console/Commands/CheckArcadiaConformityCommandTest.php b/tests/Feature/Console/Commands/CheckArcadiaConformityCommandTest.php index 5acc6d7..060ca4f 100644 --- a/tests/Feature/Console/Commands/CheckArcadiaConformityCommandTest.php +++ b/tests/Feature/Console/Commands/CheckArcadiaConformityCommandTest.php @@ -31,7 +31,7 @@ describe('CheckArcadiaConformityCommand', function (): void { $this->app->instance(HealthChecker::class, $healthChecker); // WHEN - $this->artisan('arcadia:database:check-conformity') + $this->artisan('uwil:arcadia:database:check-conformity') // THEN ->expectsOutput('Arcadia database conformity check passed. All directory probes are conform.') ->assertSuccessful(); @@ -52,7 +52,7 @@ describe('CheckArcadiaConformityCommand', function (): void { $this->app->instance(HealthChecker::class, $healthChecker); // WHEN - $this->artisan('arcadia:database:check-conformity') + $this->artisan('uwil:arcadia:database:check-conformity') // THEN ->expectsOutput('Arcadia database conformity check failed. Dissonances detected.') ->assertFailed(); @@ -73,7 +73,7 @@ describe('CheckArcadiaConformityCommand', function (): void { $this->app->instance(HealthChecker::class, $healthChecker); // WHEN - $this->artisan('arcadia:database:check-conformity', ['--json' => true]) + $this->artisan('uwil:arcadia:database:check-conformity', ['--json' => true]) // THEN ->expectsOutputToContain('"status": "healthy"') ->assertSuccessful(); @@ -94,7 +94,7 @@ describe('CheckArcadiaConformityCommand', function (): void { $this->app->instance(HealthChecker::class, $healthChecker); // WHEN - $this->artisan('arcadia:database:check-conformity', ['--json' => true]) + $this->artisan('uwil:arcadia:database:check-conformity', ['--json' => true]) // THEN ->expectsOutputToContain('"status": "unhealthy"') ->assertFailed(); diff --git a/tests/Feature/Console/Commands/CreateClientCommandTest.php b/tests/Feature/Console/Commands/CreateClientCommandTest.php index 92cde00..e050c05 100644 --- a/tests/Feature/Console/Commands/CreateClientCommandTest.php +++ b/tests/Feature/Console/Commands/CreateClientCommandTest.php @@ -16,7 +16,7 @@ describe('CreateClientCommand', function (): void { $clientName = 'Production Gateway'; // WHEN - $this->artisan('arcadia:client:create') + $this->artisan('uwil:arcadia:client:create') // THEN ->expectsQuestion('Client name (e.g. "Ones Branding website")', $clientName) ->expectsConfirmation('Create this API client?', 'yes') @@ -38,7 +38,7 @@ describe('CreateClientCommand', function (): void { $clientName = 'Cancelled Gateway'; // WHEN - $this->artisan('arcadia:client:create') + $this->artisan('uwil:arcadia:client:create') // THEN ->expectsQuestion('Client name (e.g. "Ones Branding website")', $clientName) ->expectsConfirmation('Create this API client?', 'no') @@ -51,7 +51,8 @@ describe('CreateClientCommand', function (): void { it('fails when an empty name is provided', function (): void { // GIVEN & WHEN & THEN - $this->artisan('arcadia:client:create') + /** @var \Tests\TestCase $this */ + $this->artisan('uwil:arcadia:client:create') ->expectsQuestion('Client name (e.g. "Ones Branding website")', '') ->assertFailed(); }); @@ -65,7 +66,7 @@ describe('CreateClientCommand', function (): void { ]); // WHEN & THEN - $this->artisan('arcadia:client:create') + $this->artisan('uwil:arcadia:client:create') ->expectsQuestion('Client name (e.g. "Ones Branding website")', 'Existing Gateway') ->assertFailed(); }); diff --git a/tests/Feature/Console/Commands/ExportSecurityAuditLogsCommandTest.php b/tests/Feature/Console/Commands/ExportSecurityAuditLogsCommandTest.php index 371131f..b15eae5 100644 --- a/tests/Feature/Console/Commands/ExportSecurityAuditLogsCommandTest.php +++ b/tests/Feature/Console/Commands/ExportSecurityAuditLogsCommandTest.php @@ -32,7 +32,7 @@ describe('ExportSecurityAuditLogsCommand', function (): void { ]); // WHEN - $this->artisan('arcadia:security:audit-logs', [ + $this->artisan('uwil:arcadia:security:audit-logs', [ '--since' => '2026-08-01', '--to' => '2026-08-21 23:59:59', ]) @@ -58,7 +58,7 @@ describe('ExportSecurityAuditLogsCommand', function (): void { /** @var \Tests\TestCase $this */ // WHEN - $this->artisan('arcadia:security:audit-logs', [ + $this->artisan('uwil:arcadia:security:audit-logs', [ '--since' => '2026-08-01', '--to' => '2026-08-02', ]) @@ -82,7 +82,7 @@ describe('ExportSecurityAuditLogsCommand', function (): void { ]); // WHEN - $this->artisan('arcadia:security:audit-logs', [ + $this->artisan('uwil:arcadia:security:audit-logs', [ '--since' => '2026-08-01', '--csv' => true, ]) @@ -102,7 +102,7 @@ describe('ExportSecurityAuditLogsCommand', function (): void { it('fails when an invalid date format is provided for --since', function (): void { // GIVEN & WHEN & THEN /** @var \Tests\TestCase $this */ - $this->artisan('arcadia:security:audit-logs', [ + $this->artisan('uwil:arcadia:security:audit-logs', [ '--since' => 'invalid-date-string', ]) ->expectsOutputToContain("Invalid date format for --since: 'invalid-date-string'.") diff --git a/tests/Feature/Console/Commands/HealthCheckCommandTest.php b/tests/Feature/Console/Commands/HealthCheckCommandTest.php index 007c34b..527b1ff 100644 --- a/tests/Feature/Console/Commands/HealthCheckCommandTest.php +++ b/tests/Feature/Console/Commands/HealthCheckCommandTest.php @@ -34,7 +34,7 @@ describe('HealthCheckCommand', function (): void { /** @var \Tests\TestCase $this */ // WHEN - $this->artisan('health:check') + $this->artisan('uwil:health:check') // THEN ->expectsOutput('Health check passed. All components are healthy.') ->assertSuccessful(); @@ -46,7 +46,7 @@ describe('HealthCheckCommand', function (): void { Config::set('database.default', 'invalid_connection'); // WHEN - $this->artisan('health:check') + $this->artisan('uwil:health:check') // THEN ->expectsOutput('Health check failed. One or more components are unhealthy.') ->assertFailed(); @@ -57,7 +57,7 @@ describe('HealthCheckCommand', function (): void { /** @var \Tests\TestCase $this */ // WHEN - $this->artisan('health:check', ['--json' => true]) + $this->artisan('uwil:health:check', ['--json' => true]) // THEN ->expectsOutputToContain('"status": "healthy"') ->assertSuccessful(); @@ -69,7 +69,7 @@ describe('HealthCheckCommand', function (): void { Config::set('database.default', 'invalid_connection'); // WHEN - $this->artisan('health:check', ['--json' => true]) + $this->artisan('uwil:health:check', ['--json' => true]) // THEN ->expectsOutputToContain('"status": "unhealthy"') ->assertFailed(); diff --git a/tests/Feature/Console/Commands/ListBrandsCommandTest.php b/tests/Feature/Console/Commands/ListBrandsCommandTest.php index f85b160..a71d894 100644 --- a/tests/Feature/Console/Commands/ListBrandsCommandTest.php +++ b/tests/Feature/Console/Commands/ListBrandsCommandTest.php @@ -22,7 +22,7 @@ describe('ListBrandsCommand', function (): void { /** @var \Tests\TestCase $this */ // WHEN - $this->artisan('arcadia:brand:list') + $this->artisan('uwil:arcadia:brand:list') // THEN ->expectsOutput('No brands found.') ->assertSuccessful(); @@ -48,7 +48,7 @@ describe('ListBrandsCommand', function (): void { ]); // WHEN - $this->artisan('arcadia:brand:list') + $this->artisan('uwil:arcadia:brand:list') // THEN ->expectsOutput('Showing page 1/1 (Total: 2 brand(s))') ->assertSuccessful(); @@ -68,7 +68,7 @@ describe('ListBrandsCommand', function (): void { } // WHEN - $this->artisan('arcadia:brand:list', ['--page' => '2', '--per-page' => '2']) + $this->artisan('uwil:arcadia:brand:list', ['--page' => '2', '--per-page' => '2']) // THEN ->expectsOutput('Showing page 2/2 (Total: 3 brand(s))') ->assertSuccessful(); diff --git a/tests/Feature/Console/Commands/ListClientsCommandTest.php b/tests/Feature/Console/Commands/ListClientsCommandTest.php index cb06df5..b12baa3 100644 --- a/tests/Feature/Console/Commands/ListClientsCommandTest.php +++ b/tests/Feature/Console/Commands/ListClientsCommandTest.php @@ -18,7 +18,7 @@ describe('ListClientsCommand', function (): void { ApiClient::query()->delete(); // WHEN - $this->artisan('arcadia:client:list') + $this->artisan('uwil:arcadia:client:list') // THEN ->expectsOutput('No clients found.') ->assertSuccessful(); @@ -48,7 +48,7 @@ describe('ListClientsCommand', function (): void { ]); // WHEN - $this->artisan('arcadia:client:list') + $this->artisan('uwil:arcadia:client:list') // THEN ->expectsTable( ['ID', 'Name', 'Identifier', 'Status', 'Last Updated At'], @@ -90,7 +90,7 @@ describe('ListClientsCommand', function (): void { } // WHEN - $this->artisan('arcadia:client:list', ['--page' => '2', '--per-page' => '2']) + $this->artisan('uwil:arcadia:client:list', ['--page' => '2', '--per-page' => '2']) // THEN ->expectsOutput('Showing page 2/2 (Total: 3 client(s))') ->assertSuccessful(); diff --git a/tests/Feature/Console/Commands/RegenerateClientSecretCommandTest.php b/tests/Feature/Console/Commands/RegenerateClientSecretCommandTest.php index a94e394..b800d89 100644 --- a/tests/Feature/Console/Commands/RegenerateClientSecretCommandTest.php +++ b/tests/Feature/Console/Commands/RegenerateClientSecretCommandTest.php @@ -18,7 +18,7 @@ describe('RegenerateClientSecretCommand', function (): void { $invalidIdentifier = 'invalid-id'; // WHEN - $this->artisan('arcadia:client:regenerate-secret', ['identifier' => $invalidIdentifier]) + $this->artisan('uwil:arcadia:client:regenerate-secret', ['identifier' => $invalidIdentifier]) // THEN ->expectsOutput("Invalid client identifier '{$invalidIdentifier}'. Expected a valid client identifier.") ->assertFailed(); @@ -30,7 +30,7 @@ describe('RegenerateClientSecretCommand', function (): void { $nonExistentIdentifier = 'arcadia_id_' . str_repeat('f', 32); // WHEN - $this->artisan('arcadia:client:regenerate-secret', ['identifier' => $nonExistentIdentifier]) + $this->artisan('uwil:arcadia:client:regenerate-secret', ['identifier' => $nonExistentIdentifier]) // THEN ->expectsOutput("Client '{$nonExistentIdentifier}' not found.") ->assertFailed(); @@ -52,7 +52,7 @@ describe('RegenerateClientSecretCommand', function (): void { ]); // WHEN - $this->artisan('arcadia:client:regenerate-secret', ['identifier' => $identifier]) + $this->artisan('uwil:arcadia:client:regenerate-secret', ['identifier' => $identifier]) // THEN ->expectsConfirmation('Do you want to proceed?', 'yes') ->expectsOutputToContain("Secret for client '{$identifier}' regenerated successfully.") @@ -80,7 +80,7 @@ describe('RegenerateClientSecretCommand', function (): void { ]); // WHEN - $this->artisan('arcadia:client:regenerate-secret', ['identifier' => $identifier]) + $this->artisan('uwil:arcadia:client:regenerate-secret', ['identifier' => $identifier]) // THEN ->expectsConfirmation('Do you want to proceed?', 'no') ->expectsOutput('Secret regeneration cancelled.') @@ -106,7 +106,7 @@ describe('RegenerateClientSecretCommand', function (): void { ]); // WHEN - $this->artisan('arcadia:client:regenerate-secret') + $this->artisan('uwil:arcadia:client:regenerate-secret') // THEN ->expectsQuestion('Search client (by name, ID, identifier, or slug):', $identifier) ->expectsChoice('Search client (by name, ID, identifier, or slug):', $identifier, [ diff --git a/tests/Feature/Console/Commands/ToggleClientStatusCommandTest.php b/tests/Feature/Console/Commands/ToggleClientStatusCommandTest.php index e325b67..c07343a 100644 --- a/tests/Feature/Console/Commands/ToggleClientStatusCommandTest.php +++ b/tests/Feature/Console/Commands/ToggleClientStatusCommandTest.php @@ -18,7 +18,7 @@ describe('ToggleClientStatusCommand', function (): void { $invalidId = 'not-a-valid-identifier'; // WHEN - $this->artisan('arcadia:client:toggle-status', ['identifier' => $invalidId]) + $this->artisan('uwil:arcadia:client:toggle-status', ['identifier' => $invalidId]) // THEN ->expectsOutput("Invalid client identifier '{$invalidId}'. Expected a valid client identifier.") ->assertFailed(); @@ -30,7 +30,7 @@ describe('ToggleClientStatusCommand', function (): void { $nonExistentId = 'arcadia_id_' . str_repeat('9', 32); // WHEN - $this->artisan('arcadia:client:toggle-status', ['identifier' => $nonExistentId]) + $this->artisan('uwil:arcadia:client:toggle-status', ['identifier' => $nonExistentId]) // THEN ->expectsOutput("Client '{$nonExistentId}' not found.") ->assertFailed(); @@ -49,7 +49,7 @@ describe('ToggleClientStatusCommand', function (): void { ]); // WHEN - $this->artisan('arcadia:client:toggle-status', ['identifier' => (string) $client->identifier->value]) + $this->artisan('uwil:arcadia:client:toggle-status', ['identifier' => (string) $client->identifier->value]) // THEN ->expectsOutput("Client 'Production Gateway' (ID: {$client->id}) is now Inactive.") ->assertSuccessful(); @@ -71,7 +71,7 @@ describe('ToggleClientStatusCommand', function (): void { ]); // WHEN - $this->artisan('arcadia:client:toggle-status', ['identifier' => (string) $client->identifier->value]) + $this->artisan('uwil:arcadia:client:toggle-status', ['identifier' => (string) $client->identifier->value]) // THEN ->expectsOutput("Client 'Backup Gateway' (ID: {$client->id}) is now Active.") ->assertSuccessful(); @@ -93,7 +93,7 @@ describe('ToggleClientStatusCommand', function (): void { ]); // WHEN - $this->artisan('arcadia:client:toggle-status', ['identifier' => (string) $client->identifier->value, 'status' => 'activate']) + $this->artisan('uwil:arcadia:client:toggle-status', ['identifier' => (string) $client->identifier->value, 'status' => 'activate']) // THEN ->expectsOutput("Client 'Special Gateway' (ID: {$client->id}) is now Active.") ->assertSuccessful(); @@ -115,7 +115,7 @@ describe('ToggleClientStatusCommand', function (): void { ]); // WHEN - $this->artisan('arcadia:client:toggle-status', ['identifier' => (string) $client->identifier->value, 'status' => 'deactivate']) + $this->artisan('uwil:arcadia:client:toggle-status', ['identifier' => (string) $client->identifier->value, 'status' => 'deactivate']) // THEN ->expectsOutput("Client 'Special Gateway' (ID: {$client->id}) is now Inactive.") ->assertSuccessful(); @@ -138,7 +138,7 @@ describe('ToggleClientStatusCommand', function (): void { ]); // WHEN - $this->artisan('arcadia:client:toggle-status', ['identifier' => $identifier, 'status' => 'unknown']) + $this->artisan('uwil:arcadia:client:toggle-status', ['identifier' => $identifier, 'status' => 'unknown']) // THEN ->expectsOutput("Invalid status 'unknown'. Expected 'activate' or 'deactivate'.") ->assertFailed(); @@ -158,7 +158,7 @@ describe('ToggleClientStatusCommand', function (): void { ]); // WHEN - $this->artisan('arcadia:client:toggle-status') + $this->artisan('uwil:arcadia:client:toggle-status') // THEN ->expectsQuestion('Search client (by name, ID, identifier, or slug):', $identifier) ->expectsChoice('Search client (by name, ID, identifier, or slug):', $identifier, [ diff --git a/tests/Feature/OpenApi/V1/Health/HealthOpenApiTest.php b/tests/Feature/OpenApi/V1/Health/HealthOpenApiTest.php index cd5cbd6..93dc5fe 100644 --- a/tests/Feature/OpenApi/V1/Health/HealthOpenApiTest.php +++ b/tests/Feature/OpenApi/V1/Health/HealthOpenApiTest.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace Tests\Feature\OpenApi\V1\Health; +use Illuminate\Support\Facades\Config; use Spectator\Spectator; use Tests\TestCase; @@ -26,4 +27,24 @@ describe('Health Check OpenAPI Conformance', function (): void { $response->assertValidRequest(); $response->assertValidResponse(200); }); + + it('validates GET /api/v1/health/oracle response against openapi spec', function (): void { + // GIVEN + /** @var TestCase $this */ + $originalArcadiaDb = Config::get('database.arcadia_meta_model'); + Config::set('database.arcadia_meta_model', 'sqlite'); + + try { + // WHEN + /** @var \Illuminate\Testing\TestResponse<\Symfony\Component\HttpFoundation\Response> $response */ + $response = $this->getJson('/api/v1/health/oracle'); + + // THEN + $response->assertStatus(200); + $response->assertValidRequest(); + $response->assertValidResponse(200); + } finally { + Config::set('database.arcadia_meta_model', $originalArcadiaDb); + } + }); }); diff --git a/tests/Integration/Infra/Jobs/HealthCheckJobTest.php b/tests/Integration/Infra/Jobs/HealthCheckJobTest.php index 9471ff0..f7ca76d 100644 --- a/tests/Integration/Infra/Jobs/HealthCheckJobTest.php +++ b/tests/Integration/Infra/Jobs/HealthCheckJobTest.php @@ -9,7 +9,6 @@ use Application\Health\Contracts\HealthChecker; use Application\Health\Dto\CacheCheckResult; use Application\Health\Dto\DatabaseCheckResult; use Application\Health\Dto\HealthCheckResult; -use Application\Health\Dto\HealthChecks; use Application\Health\Dto\MailCheckResult; use Application\Health\Enums\ComponentStatus; use Application\Health\Enums\HealthStatus; @@ -51,12 +50,12 @@ describe('HealthCheckJob Integration Test', function (): void { $healthyResult = new HealthCheckResult( status: HealthStatus::HEALTHY, timestamp: Utc::now(), - checks: new HealthChecks( - databaseDefault: new DatabaseCheckResult(ComponentStatus::UP, 'sqlite'), - databaseArcadia: new DatabaseCheckResult(ComponentStatus::UP, 'arcadia'), - cache: new CacheCheckResult(ComponentStatus::UP, 'file'), - mailSmtp: new MailCheckResult(ComponentStatus::UP, 'log'), - ), + 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); @@ -83,12 +82,12 @@ describe('HealthCheckJob Integration Test', function (): void { $unhealthyResult = new HealthCheckResult( status: HealthStatus::UNHEALTHY, timestamp: Utc::now(), - 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'), - ), + 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); @@ -124,12 +123,12 @@ describe('HealthCheckJob Integration Test', function (): void { $unhealthyResult = new HealthCheckResult( status: HealthStatus::UNHEALTHY, timestamp: Utc::now(), - checks: new HealthChecks( - databaseDefault: new DatabaseCheckResult(ComponentStatus::UP, 'sqlite'), - databaseArcadia: new DatabaseCheckResult(ComponentStatus::DOWN, 'arcadia', 'DB down'), - cache: new CacheCheckResult(ComponentStatus::UP, 'file'), - mailSmtp: new MailCheckResult(ComponentStatus::UP, 'log'), - ), + 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); @@ -161,12 +160,12 @@ describe('HealthCheckJob Integration Test', function (): void { $healthyResult = new HealthCheckResult( status: HealthStatus::HEALTHY, timestamp: Utc::now(), - checks: new HealthChecks( - databaseDefault: new DatabaseCheckResult(ComponentStatus::UP, 'sqlite'), - databaseArcadia: new DatabaseCheckResult(ComponentStatus::UP, 'arcadia'), - cache: new CacheCheckResult(ComponentStatus::UP, 'file'), - mailSmtp: new MailCheckResult(ComponentStatus::UP, 'log'), - ), + 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); @@ -198,12 +197,12 @@ describe('HealthCheckJob Integration Test', function (): void { $healthyResult = new HealthCheckResult( status: HealthStatus::HEALTHY, timestamp: Utc::now(), - checks: new HealthChecks( - databaseDefault: new DatabaseCheckResult(ComponentStatus::UP, 'sqlite'), - databaseArcadia: new DatabaseCheckResult(ComponentStatus::UP, 'arcadia'), - cache: new CacheCheckResult(ComponentStatus::UP, 'file'), - mailSmtp: new MailCheckResult(ComponentStatus::UP, 'log'), - ), + 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); diff --git a/tests/Unit/Application/Health/HealthCheckDtoTest.php b/tests/Unit/Application/Health/HealthCheckDtoTest.php index fa16740..cabaa66 100644 --- a/tests/Unit/Application/Health/HealthCheckDtoTest.php +++ b/tests/Unit/Application/Health/HealthCheckDtoTest.php @@ -7,7 +7,6 @@ namespace Tests\Unit\Application\Health; use Application\Health\Dto\CacheCheckResult; use Application\Health\Dto\DatabaseCheckResult; use Application\Health\Dto\HealthCheckResult; -use Application\Health\Dto\HealthChecks; use Application\Health\Dto\MailCheckResult; use Application\Health\Enums\ComponentStatus; use Application\Health\Enums\HealthStatus; @@ -79,36 +78,15 @@ describe('Health Check DTOs & Enums Unit Tests', function (): void { ->and($mailFailed->message)->toBe('Connection refused'); }); - it('evaluates HealthChecks allUp correctly', function (): void { - // GIVEN - $checksUp = new HealthChecks( - databaseDefault: new DatabaseCheckResult(ComponentStatus::UP, 'sqlite'), - databaseArcadia: new DatabaseCheckResult(ComponentStatus::UP, 'arcadia'), - cache: new CacheCheckResult(ComponentStatus::UP, 'database'), - mailSmtp: new MailCheckResult(ComponentStatus::UP, 'log'), - ); - - $checksDown = new HealthChecks( - databaseDefault: new DatabaseCheckResult(ComponentStatus::DOWN, 'sqlite', 'Failed'), - databaseArcadia: new DatabaseCheckResult(ComponentStatus::UP, 'arcadia'), - cache: new CacheCheckResult(ComponentStatus::UP, 'database'), - mailSmtp: new MailCheckResult(ComponentStatus::UP, 'log'), - ); - - // WHEN & THEN - expect($checksUp->isAllUp())->toBeTrue() - ->and($checksDown->isAllUp())->toBeFalse(); - }); - it('instantiates HealthCheckResult and evaluates isHealthy', function (): void { // GIVEN $date = new \DateTimeImmutable('2026-08-21 14:30:00 UTC'); - $checks = new HealthChecks( - databaseDefault: new DatabaseCheckResult(ComponentStatus::UP, 'sqlite'), - databaseArcadia: new DatabaseCheckResult(ComponentStatus::DOWN, 'arcadia', 'Timeout connecting to DB'), - cache: new CacheCheckResult(ComponentStatus::UP, 'database'), - mailSmtp: new MailCheckResult(ComponentStatus::UP, 'smtp'), - ); + $checks = [ + 'database_default' => new DatabaseCheckResult(ComponentStatus::UP, 'sqlite'), + 'database_arcadia' => new DatabaseCheckResult(ComponentStatus::DOWN, 'arcadia', 'Timeout connecting to DB'), + 'cache' => new CacheCheckResult(ComponentStatus::UP, 'database'), + 'mail_smtp' => new MailCheckResult(ComponentStatus::UP, 'smtp'), + ]; $result = new HealthCheckResult( status: HealthStatus::UNHEALTHY, @@ -120,6 +98,6 @@ describe('Health Check DTOs & Enums Unit Tests', function (): void { expect($result->isHealthy())->toBeFalse() ->and($result->status)->toBe(HealthStatus::UNHEALTHY) ->and($result->timestamp)->toBe($date) - ->and($result->checks->databaseArcadia->message)->toBe('Timeout connecting to DB'); + ->and($result->checks['database_arcadia']->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 index 9fe90c9..8f630f6 100644 --- a/tests/Unit/Presentation/Api/V1/Http/Responses/Health/HealthCheckResponseTest.php +++ b/tests/Unit/Presentation/Api/V1/Http/Responses/Health/HealthCheckResponseTest.php @@ -7,7 +7,6 @@ namespace Tests\Unit\Presentation\Api\V1\Http\Responses\Health; use Application\Health\Dto\CacheCheckResult; use Application\Health\Dto\DatabaseCheckResult; use Application\Health\Dto\HealthCheckResult; -use Application\Health\Dto\HealthChecks; use Application\Health\Dto\MailCheckResult; use Application\Health\Enums\ComponentStatus; use Application\Health\Enums\HealthStatus; @@ -19,12 +18,15 @@ describe('HealthCheckResponse Unit Test', function (): void { it('serializes healthy HealthCheckResult into 200 JsonResponse and array', function (): void { // GIVEN $date = new \DateTimeImmutable('2026-08-27 15:00:00 UTC'); - $checks = new HealthChecks( - databaseDefault: new DatabaseCheckResult(ComponentStatus::UP, 'sqlite'), - databaseArcadia: new DatabaseCheckResult(ComponentStatus::UP, 'arcadia'), - cache: new CacheCheckResult(ComponentStatus::UP, 'file'), - mailSmtp: new MailCheckResult(ComponentStatus::UP, 'log'), - ); + $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'), + ]; + + $databaseDefaultKey = $checks['database_default']->getName(); + $databaseArcadiaKey = $checks['database_arcadia']->getName(); $result = new HealthCheckResult(HealthStatus::HEALTHY, $date, $checks); $response = new HealthCheckResponse($result); @@ -37,19 +39,21 @@ describe('HealthCheckResponse Unit Test', function (): void { 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'); + ->and($array['checks'][$databaseDefaultKey]['status'])->toBe('up') + ->and($array['checks'][$databaseArcadiaKey]['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'), - ); + $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'), + ]; + + $databaseDefaultKey = $checks['database_default']->getName(); $result = new HealthCheckResult(HealthStatus::UNHEALTHY, $date, $checks); $response = new HealthCheckResponse($result); @@ -61,6 +65,6 @@ describe('HealthCheckResponse Unit Test', function (): void { // THEN expect($json->getStatusCode())->toBe(503) ->and($array['status'])->toBe('unhealthy') - ->and($array['checks']['database_default']['message'])->toBe('Connection refused'); + ->and($array['checks'][$databaseDefaultKey]['message'])->toBe('Connection refused'); }); }); diff --git a/tests/Unit/Presentation/Api/V1/Http/Responses/Health/OracleHealthCheckResponseTest.php b/tests/Unit/Presentation/Api/V1/Http/Responses/Health/OracleHealthCheckResponseTest.php new file mode 100644 index 0000000..727f138 --- /dev/null +++ b/tests/Unit/Presentation/Api/V1/Http/Responses/Health/OracleHealthCheckResponseTest.php @@ -0,0 +1,51 @@ +toArray(); + $json = $response->present(); + + // THEN + expect($json->getStatusCode())->toBe(200) + ->and($array['status'])->toBe('healthy') + ->and($array['timestamp'])->toBe('2026-10-06T10:00:00+00:00') + ->and($array['oracle']['status'])->toBe('up') + ->and($array['oracle']['connection'])->toBe('arcadia'); + }); + + it('serializes unhealthy oracle connectivity into 503 JsonResponse and array', function (): void { + // GIVEN + $date = new \DateTimeImmutable('2026-10-06 10:00:00 UTC'); + $response = new OracleHealthCheckResponse( + result: new DatabaseCheckResult(ComponentStatus::DOWN, 'arcadia', 'Connection refused'), + timestamp: $date, + ); + + // WHEN + $array = $response->toArray(); + $json = $response->present(); + + // THEN + expect($json->getStatusCode())->toBe(503) + ->and($array['status'])->toBe('unhealthy') + ->and($array['oracle']['status'])->toBe('down'); + }); +});