From 75d5e9cf5474ef035e1459f1502a12c511bc2657 Mon Sep 17 00:00:00 2001 From: tristan Date: Thu, 21 May 2026 14:07:09 +0200 Subject: [PATCH] feat(fer-19) : checkHealth sur le PontBasculeService Co-Authored-By: Claude Opus 4.7 (1M context) --- src/Service/PontBasculeService.php | 51 +++++++++++- tests/Service/PontBasculeServiceTest.php | 99 ++++++++++++++++++++++++ 2 files changed, 149 insertions(+), 1 deletion(-) diff --git a/src/Service/PontBasculeService.php b/src/Service/PontBasculeService.php index 6c1237c..51fd223 100644 --- a/src/Service/PontBasculeService.php +++ b/src/Service/PontBasculeService.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace App\Service; +use App\Dto\PontBasculeHealth; use App\Dto\PontBasculeReading; use App\Exception\PontBasculeException; use DateTimeImmutable; @@ -28,7 +29,7 @@ final class PontBasculeService $body = $this->getBypassPayload(); } else { try { - $response = $this->httpClient->request('POST', $this->baseUrl.'/send/dsd'); + $response = $this->httpClient->request('POST', $this->buildUrl('/send/dsd')); $body = $response->getContent(false); } catch (TransportExceptionInterface $exception) { throw PontBasculeException::transportFailure($exception->getMessage()); @@ -44,6 +45,54 @@ final class PontBasculeService ); } + public function checkHealth(): PontBasculeHealth + { + if ($this->bypass) { + return new PontBasculeHealth( + healthy: true, + ok: true, + busy: false, + portConnected: true, + portError: null, + hostname: 'bypass', + ); + } + + try { + $response = $this->httpClient->request('GET', $this->buildUrl('/health')); + $body = $response->getContent(false); + } catch (TransportExceptionInterface) { + return PontBasculeHealth::unhealthy(); + } + + $payload = json_decode($body, true); + if (!is_array($payload)) { + return PontBasculeHealth::unhealthy(); + } + + $ok = true === ($payload['ok'] ?? null); + $busy = true === ($payload['busy'] ?? null); + $portConnected = true === ($payload['port_connected'] ?? null); + $portError = $payload['port_error'] ?? null; + $hostname = $payload['hostname'] ?? null; + + $healthy = $ok && $portConnected && !$busy && null === $portError; + + return new PontBasculeHealth( + healthy: $healthy, + ok: $ok, + busy: $busy, + portConnected: $portConnected, + portError: is_string($portError) ? $portError : null, + hostname: is_string($hostname) ? $hostname : null, + ); + } + + private function buildUrl(string $path): string + { + return rtrim($this->baseUrl, '/').$path; + } + private function getBypassPayload(): string { return '{"ok":true,"busy":false,"mode":"serial","port":"/dev/ttyUSB0","baudrate":9600,"request_hex":"01 10 39 39 4D 0D 0A","response_hex":"01 02 30 34 30 32 30 30 02 30 31 30 30 31 34 32 30 2E 6B 67 20 02 30 32 30 30 30 30 30 30 2E 6B 67 20 02 30 33 30 30 31 34 32 30 2E 6B 67 20 02 39 39 30 30 31 32 31 0D 0A","response_ascii":"\u0001\u0002040200\u000201001420.kg \u000202000000.kg \u000203001420.kg \u00029900121"}'; diff --git a/tests/Service/PontBasculeServiceTest.php b/tests/Service/PontBasculeServiceTest.php index 543f55a..d2425f3 100644 --- a/tests/Service/PontBasculeServiceTest.php +++ b/tests/Service/PontBasculeServiceTest.php @@ -82,4 +82,103 @@ final class PontBasculeServiceTest extends TestCase $service->fetch(); } + + public function testCheckHealthBypassIsHealthyWithoutHttpCall(): void + { + $httpClient = $this->createMock(HttpClientInterface::class); + $httpClient->expects(self::never())->method('request'); + + $service = new PontBasculeService($httpClient, new PontBasculePayloadDecoder(), 'http://example.test', true); + + $health = $service->checkHealth(); + + self::assertTrue($health->isHealthy()); + } + + public function testCheckHealthHealthyPayload(): void + { + $service = $this->serviceForHealthBody(json_encode([ + 'ok' => true, + 'busy' => false, + 'port_connected' => true, + 'port_error' => null, + 'hostname' => 'liot-rasp-ferme-01', + ], JSON_THROW_ON_ERROR)); + + $health = $service->checkHealth(); + + self::assertTrue($health->isHealthy()); + self::assertSame('liot-rasp-ferme-01', $health->getHostname()); + } + + public function testCheckHealthUnhealthyWhenPortError(): void + { + $service = $this->serviceForHealthBody(json_encode([ + 'ok' => true, + 'busy' => false, + 'port_connected' => true, + 'port_error' => 'device disconnected', + ], JSON_THROW_ON_ERROR)); + + self::assertFalse($service->checkHealth()->isHealthy()); + } + + public function testCheckHealthUnhealthyWhenPortNotConnected(): void + { + $service = $this->serviceForHealthBody(json_encode([ + 'ok' => true, + 'busy' => false, + 'port_connected' => false, + 'port_error' => null, + ], JSON_THROW_ON_ERROR)); + + self::assertFalse($service->checkHealth()->isHealthy()); + } + + public function testCheckHealthUnhealthyWhenBusy(): void + { + $service = $this->serviceForHealthBody(json_encode([ + 'ok' => true, + 'busy' => true, + 'port_connected' => true, + 'port_error' => null, + ], JSON_THROW_ON_ERROR)); + + self::assertFalse($service->checkHealth()->isHealthy()); + } + + public function testCheckHealthUnhealthyOnTransportFailure(): void + { + $httpClient = $this->createMock(HttpClientInterface::class); + $httpClient + ->expects(self::once()) + ->method('request') + ->willThrowException($this->createStub(TransportExceptionInterface::class)) + ; + + $service = new PontBasculeService($httpClient, new PontBasculePayloadDecoder(), 'http://example.test', false); + + self::assertFalse($service->checkHealth()->isHealthy()); + } + + public function testCheckHealthUnhealthyOnInvalidJson(): void + { + self::assertFalse($this->serviceForHealthBody('not-json')->checkHealth()->isHealthy()); + } + + private function serviceForHealthBody(string $body): PontBasculeService + { + $response = $this->createStub(ResponseInterface::class); + $response->method('getContent')->with(false)->willReturn($body); + + $httpClient = $this->createMock(HttpClientInterface::class); + $httpClient + ->expects(self::once()) + ->method('request') + ->with('GET', 'http://example.test/health') + ->willReturn($response) + ; + + return new PontBasculeService($httpClient, new PontBasculePayloadDecoder(), 'http://example.test', false); + } }