Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dd5987329d | ||
| cde30340a1 | |||
|
|
7e368505e4 | ||
| fd6aed8ae0 |
2
.env
2
.env
@@ -19,4 +19,4 @@ COOKIE_SECURE=
|
|||||||
DATABASE_URL=
|
DATABASE_URL=
|
||||||
|
|
||||||
PONT_BASCULE_BYPASS=
|
PONT_BASCULE_BYPASS=
|
||||||
PONT_BASCULE_URL=
|
PONT_BASCULE_BASE_URL=
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ Ajouter dans le fichier .env du frontend
|
|||||||
* [#FER-25] Ajout un cron pour la synchro de l'inventaire bovin
|
* [#FER-25] Ajout un cron pour la synchro de l'inventaire bovin
|
||||||
* [#FER-22] Pouvoir exporter les réceptions/expéditions fines en Excel
|
* [#FER-22] Pouvoir exporter les réceptions/expéditions fines en Excel
|
||||||
* [#FER-30] Revoir l'affichage type bovin
|
* [#FER-30] Revoir l'affichage type bovin
|
||||||
|
* [#FER-19] Ajouter le healthCheck du pont bascule
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ services:
|
|||||||
|
|
||||||
App\Service\PontBasculeService:
|
App\Service\PontBasculeService:
|
||||||
arguments:
|
arguments:
|
||||||
$endpoint: '%env(PONT_BASCULE_URL)%'
|
$baseUrl: '%env(PONT_BASCULE_BASE_URL)%'
|
||||||
$bypass: '%env(bool:PONT_BASCULE_BYPASS)%'
|
$bypass: '%env(bool:PONT_BASCULE_BYPASS)%'
|
||||||
|
|
||||||
# add more service definitions when explicit configuration is needed
|
# add more service definitions when explicit configuration is needed
|
||||||
|
|||||||
@@ -1,2 +1,2 @@
|
|||||||
parameters:
|
parameters:
|
||||||
app.version: '0.0.105'
|
app.version: '0.0.107'
|
||||||
|
|||||||
647
docs/superpowers/plans/2026-05-21-pont-bascule-healthcheck.md
Normal file
647
docs/superpowers/plans/2026-05-21-pont-bascule-healthcheck.md
Normal file
@@ -0,0 +1,647 @@
|
|||||||
|
# Health-check pont-bascule sur l'écran de pesée — Implementation Plan
|
||||||
|
|
||||||
|
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||||
|
|
||||||
|
**Goal:** Brancher le vrai health-check du pont-bascule à l'arrivée sur l'écran de pesée, afficher l'état réel et désactiver le bouton « peser » tant que le pont n'est pas valide.
|
||||||
|
|
||||||
|
**Architecture:** Une URL de base unique (`PONT_BASCULE_BASE_URL`) ; le `PontBasculeService` construit `/send/dsd` et `/health`. Un nouvel endpoint générique `GET /pont_bascule/health` (carrier API Platform + DTO + provider) renvoie toujours `200 { healthy: bool, ... }`. Le front interroge cet endpoint une fois au montage de `WorkflowWeight`, affiche l'état et grise le bouton « peser » si non sain. Le bypass dev renvoie un état sain.
|
||||||
|
|
||||||
|
**Tech Stack:** Symfony 8 / API Platform 4 (PHP 8.4, PHPUnit, MockHttpClient) ; Nuxt 4 (Vue 3, composables).
|
||||||
|
|
||||||
|
**Spec:** `docs/superpowers/specs/2026-05-21-pont-bascule-healthcheck-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
**Backend (créés) :**
|
||||||
|
- `src/Dto/PontBasculeHealth.php` — DTO domaine : `healthy` + champs informatifs, getters, Groups, factory `unhealthy()`.
|
||||||
|
- `src/ApiResource/PontBasculeHealthCheck.php` — carrier API Platform hébergeant `GET /pont_bascule/health`, `output: PontBasculeHealth::class`.
|
||||||
|
- `src/State/PontBasculeHealthProvider.php` — provider qui appelle `PontBasculeService::checkHealth()`.
|
||||||
|
|
||||||
|
**Backend (modifiés) :**
|
||||||
|
- `src/Service/PontBasculeService.php` — `$endpoint` → `$baseUrl` ; `fetch()` POST `{base}/send/dsd` ; nouvelle méthode `checkHealth()`.
|
||||||
|
- `config/services.yaml` — argument `$baseUrl` ← `PONT_BASCULE_BASE_URL`.
|
||||||
|
- `.env`, `.env.local`, `.env.prod` — `PONT_BASCULE_URL` → `PONT_BASCULE_BASE_URL` (sans `/send/dsd`).
|
||||||
|
- `tests/Service/PontBasculeServiceTest.php` — adapter l'URL attendue + tests `checkHealth()`.
|
||||||
|
|
||||||
|
**Frontend (créés) :**
|
||||||
|
- `frontend/composables/usePontBascule.ts` — état `status` + `checkHealth()`.
|
||||||
|
|
||||||
|
**Frontend (modifiés) :**
|
||||||
|
- `frontend/components/workflow/workflow-weight.vue` — affichage dynamique de l'état + `disabled` sur « peser ».
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 1 : Refactor env vers une URL de base unique
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/Service/PontBasculeService.php:15-36`
|
||||||
|
- Modify: `config/services.yaml:25-28`
|
||||||
|
- Modify: `.env:21-22`, `.env.local:21-22`, `.env.prod:21-22`
|
||||||
|
- Test: `tests/Service/PontBasculeServiceTest.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1 : Adapter le test existant à l'URL `/send/dsd`**
|
||||||
|
|
||||||
|
Dans `tests/Service/PontBasculeServiceTest.php`, méthode `testFetchUsesHttpClientWhenNotBypass`, remplacer l'attente d'URL :
|
||||||
|
|
||||||
|
```php
|
||||||
|
$httpClient
|
||||||
|
->expects(self::once())
|
||||||
|
->method('request')
|
||||||
|
->with('POST', 'http://example.test/send/dsd')
|
||||||
|
->willReturn($response)
|
||||||
|
;
|
||||||
|
```
|
||||||
|
|
||||||
|
(Les autres `'http://example.test'` passés au constructeur restent la **base** — inchangés.)
|
||||||
|
|
||||||
|
- [ ] **Step 2 : Lancer le test, vérifier l'échec**
|
||||||
|
|
||||||
|
Run: `make test FILES=tests/Service/PontBasculeServiceTest.php`
|
||||||
|
Expected: FAIL sur `testFetchUsesHttpClientWhenNotBypass` (l'implémentation appelle encore `http://example.test`).
|
||||||
|
|
||||||
|
- [ ] **Step 3 : Renommer `$endpoint` → `$baseUrl` et construire `/send/dsd`**
|
||||||
|
|
||||||
|
Dans `src/Service/PontBasculeService.php`, constructeur et `fetch()` :
|
||||||
|
|
||||||
|
```php
|
||||||
|
public function __construct(
|
||||||
|
private readonly HttpClientInterface $httpClient,
|
||||||
|
private readonly PontBasculePayloadDecoder $payloadDecoder,
|
||||||
|
private readonly string $baseUrl,
|
||||||
|
private readonly bool $bypass,
|
||||||
|
) {}
|
||||||
|
```
|
||||||
|
|
||||||
|
et dans `fetch()` remplacer la ligne de requête :
|
||||||
|
|
||||||
|
```php
|
||||||
|
$response = $this->httpClient->request('POST', $this->baseUrl . '/send/dsd');
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4 : Lancer le test, vérifier le succès**
|
||||||
|
|
||||||
|
Run: `make test FILES=tests/Service/PontBasculeServiceTest.php`
|
||||||
|
Expected: PASS (3 tests).
|
||||||
|
|
||||||
|
- [ ] **Step 5 : Mettre à jour `config/services.yaml`**
|
||||||
|
|
||||||
|
Remplacer le bloc du service (lignes 25-28) :
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
App\Service\PontBasculeService:
|
||||||
|
arguments:
|
||||||
|
$baseUrl: '%env(PONT_BASCULE_BASE_URL)%'
|
||||||
|
$bypass: '%env(bool:PONT_BASCULE_BYPASS)%'
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6 : Mettre à jour les fichiers `.env`**
|
||||||
|
|
||||||
|
Dans `.env` (lignes 21-22), remplacer :
|
||||||
|
|
||||||
|
```
|
||||||
|
PONT_BASCULE_BYPASS=
|
||||||
|
PONT_BASCULE_BASE_URL=
|
||||||
|
```
|
||||||
|
|
||||||
|
Dans `.env.local` (lignes 21-22), remplacer :
|
||||||
|
|
||||||
|
```
|
||||||
|
PONT_BASCULE_BYPASS=true
|
||||||
|
PONT_BASCULE_BASE_URL="http://100.122.43.54:8000"
|
||||||
|
```
|
||||||
|
|
||||||
|
Dans `.env.prod` (lignes 21-22), remplacer :
|
||||||
|
|
||||||
|
```
|
||||||
|
PONT_BASCULE_BYPASS=true
|
||||||
|
PONT_BASCULE_BASE_URL="http://100.122.43.54:8000"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 7 : Vider le cache et relancer toute la suite**
|
||||||
|
|
||||||
|
Run: `make cache-clear && make test`
|
||||||
|
Expected: PASS (9 tests, comme avant).
|
||||||
|
|
||||||
|
- [ ] **Step 8 : Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Service/PontBasculeService.php config/services.yaml .env .env.local .env.prod tests/Service/PontBasculeServiceTest.php
|
||||||
|
git commit -m "refactor(fer-19) : url de base unique pour le pont-bascule"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 2 : DTO `PontBasculeHealth`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/Dto/PontBasculeHealth.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1 : Créer le DTO**
|
||||||
|
|
||||||
|
`src/Dto/PontBasculeHealth.php` — mêmes conventions que `PontBasculeReading` (readonly, Groups sur les props promues, getters) :
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Dto;
|
||||||
|
|
||||||
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
|
||||||
|
final readonly class PontBasculeHealth
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
#[Groups(['pont_bascule:health:read'])]
|
||||||
|
private bool $healthy,
|
||||||
|
#[Groups(['pont_bascule:health:read'])]
|
||||||
|
private bool $ok = false,
|
||||||
|
#[Groups(['pont_bascule:health:read'])]
|
||||||
|
private bool $busy = false,
|
||||||
|
#[Groups(['pont_bascule:health:read'])]
|
||||||
|
private bool $portConnected = false,
|
||||||
|
#[Groups(['pont_bascule:health:read'])]
|
||||||
|
private ?string $portError = null,
|
||||||
|
#[Groups(['pont_bascule:health:read'])]
|
||||||
|
private ?string $hostname = null,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function unhealthy(): self
|
||||||
|
{
|
||||||
|
return new self(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isHealthy(): bool
|
||||||
|
{
|
||||||
|
return $this->healthy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isOk(): bool
|
||||||
|
{
|
||||||
|
return $this->ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isBusy(): bool
|
||||||
|
{
|
||||||
|
return $this->busy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isPortConnected(): bool
|
||||||
|
{
|
||||||
|
return $this->portConnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPortError(): ?string
|
||||||
|
{
|
||||||
|
return $this->portError;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHostname(): ?string
|
||||||
|
{
|
||||||
|
return $this->hostname;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2 : Vérifier que PHP parse le fichier**
|
||||||
|
|
||||||
|
Run: `make shell` puis `php -l src/Dto/PontBasculeHealth.php` — ou directement :
|
||||||
|
`docker exec -u www-data php-ferme-fpm php -l src/Dto/PontBasculeHealth.php`
|
||||||
|
Expected: `No syntax errors detected`.
|
||||||
|
|
||||||
|
- [ ] **Step 3 : Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Dto/PontBasculeHealth.php
|
||||||
|
git commit -m "feat(fer-19) : dto PontBasculeHealth"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 3 : Méthode `PontBasculeService::checkHealth()`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/Service/PontBasculeService.php`
|
||||||
|
- Test: `tests/Service/PontBasculeServiceTest.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1 : Écrire les tests `checkHealth()`**
|
||||||
|
|
||||||
|
Ajouter dans `tests/Service/PontBasculeServiceTest.php` (le `use App\Dto\PontBasculeHealth;` n'est pas requis, on lit via getters). Ajouter ces méthodes à la classe :
|
||||||
|
|
||||||
|
```php
|
||||||
|
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->createMock(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);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2 : Lancer les tests, vérifier l'échec**
|
||||||
|
|
||||||
|
Run: `make test FILES=tests/Service/PontBasculeServiceTest.php`
|
||||||
|
Expected: FAIL (`checkHealth()` n'existe pas → Error).
|
||||||
|
|
||||||
|
- [ ] **Step 3 : Implémenter `checkHealth()`**
|
||||||
|
|
||||||
|
Dans `src/Service/PontBasculeService.php`, ajouter `use App\Dto\PontBasculeHealth;` en tête, puis ajouter la méthode après `fetch()` :
|
||||||
|
|
||||||
|
```php
|
||||||
|
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->baseUrl . '/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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4 : Lancer les tests, vérifier le succès**
|
||||||
|
|
||||||
|
Run: `make test FILES=tests/Service/PontBasculeServiceTest.php`
|
||||||
|
Expected: PASS (10 tests : 3 `fetch` + 7 `checkHealth`).
|
||||||
|
|
||||||
|
- [ ] **Step 5 : Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/Service/PontBasculeService.php tests/Service/PontBasculeServiceTest.php
|
||||||
|
git commit -m "feat(fer-19) : checkHealth sur le PontBasculeService"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 4 : Endpoint `GET /pont_bascule/health`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `src/State/PontBasculeHealthProvider.php`
|
||||||
|
- Create: `src/ApiResource/PontBasculeHealthCheck.php`
|
||||||
|
|
||||||
|
- [ ] **Step 1 : Créer le provider**
|
||||||
|
|
||||||
|
`src/State/PontBasculeHealthProvider.php` (renvoie le DTO directement, comme `ReceptionWeighingProvider`, mais sans jamais lever d'exception) :
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\State;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use ApiPlatform\State\ProviderInterface;
|
||||||
|
use App\Dto\PontBasculeHealth;
|
||||||
|
use App\Service\PontBasculeService;
|
||||||
|
|
||||||
|
final readonly class PontBasculeHealthProvider implements ProviderInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private PontBasculeService $pontBasculeService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): PontBasculeHealth
|
||||||
|
{
|
||||||
|
return $this->pontBasculeService->checkHealth();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2 : Créer la ressource carrier**
|
||||||
|
|
||||||
|
`src/ApiResource/PontBasculeHealthCheck.php` (calque de `AppVersion` pour le style standalone, avec `output` séparé comme `/receptions/weigh`) :
|
||||||
|
|
||||||
|
```php
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\ApiResource;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
|
||||||
|
use App\Dto\PontBasculeHealth;
|
||||||
|
use App\State\PontBasculeHealthProvider;
|
||||||
|
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(
|
||||||
|
uriTemplate: '/pont_bascule/health',
|
||||||
|
openapi: new OpenApiOperation(
|
||||||
|
summary: 'Pont-bascule health check',
|
||||||
|
description: 'Returns the connection state of the pont-bascule. Always 200, even when unreachable.',
|
||||||
|
),
|
||||||
|
normalizationContext: ['groups' => ['pont_bascule:health:read']],
|
||||||
|
output: PontBasculeHealth::class,
|
||||||
|
provider: PontBasculeHealthProvider::class,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)]
|
||||||
|
final class PontBasculeHealthCheck {}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3 : Vider le cache et vérifier que la route est déclarée**
|
||||||
|
|
||||||
|
Run: `make cache-clear && docker exec -u www-data php-ferme-fpm php bin/console debug:router | grep pont_bascule`
|
||||||
|
Expected: une ligne listant `GET /api/pont_bascule/health`.
|
||||||
|
|
||||||
|
- [ ] **Step 4 : Vérifier la réponse en bypass (sain)**
|
||||||
|
|
||||||
|
Le bypass est actif en dev (`.env.local`). Appeler l'endpoint authentifié n'étant pas trivial, vérifier d'abord sans auth que la route répond (401 attendu = route existe et sécurité globale `ROLE_USER` active) :
|
||||||
|
|
||||||
|
Run: `curl -s -o /dev/null -w "%{http_code}\n" http://localhost:8080/api/pont_bascule/health`
|
||||||
|
Expected: `401` (route trouvée, auth requise) — **pas** `404`.
|
||||||
|
|
||||||
|
Si tu disposes d'un token JWT de dev, vérifier le corps :
|
||||||
|
Run: `curl -s http://localhost:8080/api/pont_bascule/health -H "Authorization: Bearer <token>"`
|
||||||
|
Expected: JSON contenant `"healthy":true` (bypass actif).
|
||||||
|
|
||||||
|
- [ ] **Step 5 : Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/State/PontBasculeHealthProvider.php src/ApiResource/PontBasculeHealthCheck.php
|
||||||
|
git commit -m "feat(fer-19) : endpoint GET /pont_bascule/health"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 5 : Composable `usePontBascule`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: `frontend/composables/usePontBascule.ts`
|
||||||
|
|
||||||
|
- [ ] **Step 1 : Créer le composable**
|
||||||
|
|
||||||
|
`frontend/composables/usePontBascule.ts` (mêmes conventions que `useAppVersion` : `useApi`, `toast: false` pour ne pas afficher d'erreur si le pont est down) :
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { useApi } from '~/composables/useApi'
|
||||||
|
|
||||||
|
export type PontBasculeStatus = 'checking' | 'connected' | 'disconnected'
|
||||||
|
|
||||||
|
export const usePontBascule = () => {
|
||||||
|
const api = useApi()
|
||||||
|
const status = ref<PontBasculeStatus>('checking')
|
||||||
|
|
||||||
|
const checkHealth = async () => {
|
||||||
|
status.value = 'checking'
|
||||||
|
try {
|
||||||
|
const res = await api.get<{ healthy: boolean }>('pont_bascule/health', {}, {
|
||||||
|
toast: false
|
||||||
|
})
|
||||||
|
status.value = res.healthy ? 'connected' : 'disconnected'
|
||||||
|
} catch {
|
||||||
|
status.value = 'disconnected'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status, checkHealth }
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2 : Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add frontend/composables/usePontBascule.ts
|
||||||
|
git commit -m "feat(fer-19) : composable usePontBascule"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Task 6 : Affichage de l'état + désactivation du bouton dans `WorkflowWeight`
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `frontend/components/workflow/workflow-weight.vue:5` (texte) et `:19-23` (bouton peser) et `<script setup>`
|
||||||
|
|
||||||
|
- [ ] **Step 1 : Remplacer le texte hardcodé par l'affichage dynamique**
|
||||||
|
|
||||||
|
Dans `frontend/components/workflow/workflow-weight.vue`, remplacer la ligne 5 :
|
||||||
|
|
||||||
|
```html
|
||||||
|
<p class="text-primary-500 uppercase text-2xl mt-2">Pont-bascule connecté</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
par :
|
||||||
|
|
||||||
|
```html
|
||||||
|
<p
|
||||||
|
v-if="pontBasculeStatus === 'checking'"
|
||||||
|
class="uppercase text-2xl mt-2 text-primary-500">Vérification du pont-bascule…</p>
|
||||||
|
<p
|
||||||
|
v-else-if="pontBasculeStatus === 'connected'"
|
||||||
|
class="uppercase text-2xl mt-2 text-green-600">Pont-bascule connecté</p>
|
||||||
|
<p
|
||||||
|
v-else
|
||||||
|
class="uppercase text-2xl mt-2 text-red-600">Pont-bascule non connecté</p>
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2 : Désactiver le bouton « peser » tant que non connecté**
|
||||||
|
|
||||||
|
Toujours dans le template, ajouter `:disabled` sur le **premier** `UiButton` (celui qui déclenche `fetchWeight`) :
|
||||||
|
|
||||||
|
```html
|
||||||
|
<UiButton
|
||||||
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
:disabled="pontBasculeStatus !== 'connected'"
|
||||||
|
@click="fetchWeight"
|
||||||
|
>{{ displayWeight !== null ? 'refaire une pesée' : 'peser' }}</UiButton>
|
||||||
|
```
|
||||||
|
|
||||||
|
(Ne pas toucher aux boutons « Valider la pesée » / « Générer le bon ».)
|
||||||
|
|
||||||
|
- [ ] **Step 3 : Brancher le composable dans `<script setup>`**
|
||||||
|
|
||||||
|
Dans le bloc `<script setup lang="ts">`, ajouter l'import en tête (à côté de `import { toRef } from 'vue'`) :
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { onMounted, toRef } from 'vue'
|
||||||
|
import { usePontBascule } from '~/composables/usePontBascule'
|
||||||
|
```
|
||||||
|
|
||||||
|
puis, après la ligne `const entityRef = toRef(props, 'entity')`, ajouter :
|
||||||
|
|
||||||
|
```ts
|
||||||
|
const { status: pontBasculeStatus, checkHealth } = usePontBascule()
|
||||||
|
onMounted(checkHealth)
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4 : Build front pour vérifier qu'il n'y a pas d'erreur de compilation/type**
|
||||||
|
|
||||||
|
Run: `cd frontend && npm run build:dist`
|
||||||
|
Expected: build OK, aucune erreur TypeScript ni de template.
|
||||||
|
|
||||||
|
- [ ] **Step 5 : Vérification manuelle (bypass on = sain)**
|
||||||
|
|
||||||
|
Avec `PONT_BASCULE_BYPASS=true` (dev), lancer le front (`cd frontend && npm run dev`), aller sur une réception à l'étape de pesée :
|
||||||
|
- Au chargement : bref « Vérification du pont-bascule… », puis « Pont-bascule connecté » (vert).
|
||||||
|
- Le bouton « peser » est actif et fonctionne comme avant.
|
||||||
|
|
||||||
|
- [ ] **Step 6 : Vérification manuelle (état non sain = bouton grisé)**
|
||||||
|
|
||||||
|
Simuler un pont down sans toucher au backend : dans l'onglet réseau / via un override temporaire, forcer la réponse de `pont_bascule/health` à `{ "healthy": false }` (ou couper `PONT_BASCULE_BYPASS` et pointer une base URL injoignable). Recharger l'écran de pesée :
|
||||||
|
- Texte « Pont-bascule non connecté » (rouge).
|
||||||
|
- Bouton « peser » grisé (`opacity-60`, `cursor-not-allowed`) et non cliquable.
|
||||||
|
|
||||||
|
Rétablir `PONT_BASCULE_BYPASS=true` après le test.
|
||||||
|
|
||||||
|
- [ ] **Step 7 : Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add frontend/components/workflow/workflow-weight.vue
|
||||||
|
git commit -m "feat(fer-19) : affichage etat pont-bascule et desactivation du bouton peser"
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Vérification finale
|
||||||
|
|
||||||
|
- [ ] **Suite backend complète**
|
||||||
|
|
||||||
|
Run: `make test`
|
||||||
|
Expected: PASS (16 tests : 9 existants + 7 nouveaux `checkHealth`).
|
||||||
|
|
||||||
|
- [ ] **Style PHP**
|
||||||
|
|
||||||
|
Run: `make php-cs-fixer-allow-risky FILES=src/Service/PontBasculeService.php src/Dto/PontBasculeHealth.php src/ApiResource/PontBasculeHealthCheck.php src/State/PontBasculeHealthProvider.php`
|
||||||
|
Expected: fichiers conformes (aucune correction ou corrections appliquées puis re-commit).
|
||||||
|
|
||||||
|
- [ ] **Route présente**
|
||||||
|
|
||||||
|
Run: `docker exec -u www-data php-ferme-fpm php bin/console debug:router | grep pont_bascule`
|
||||||
|
Expected: `GET /api/pont_bascule/health`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notes de risque
|
||||||
|
|
||||||
|
- **Carrier + `output` sur ressource non-entité :** si API Platform refuse `output:` sur la classe carrier vide `PontBasculeHealthCheck`, repli sur le pattern `AppVersion` pur — déplacer les champs (props publiques + Groups) directement dans `PontBasculeHealthCheck`, supprimer `output:`, et faire que `PontBasculeHealthProvider` mappe le DTO `PontBasculeHealth` vers une instance de `PontBasculeHealthCheck`. Le comportement HTTP reste identique. (Le repli ne change que le câblage interne, pas l'API ni le front.)
|
||||||
|
- **Check unique au montage (choix produit) :** si le pont est down à l'arrivée, le bouton reste grisé jusqu'au rechargement de la page. Conforme à la spec ; pas de polling ni de bouton « réessayer » dans ce périmètre.
|
||||||
@@ -0,0 +1,174 @@
|
|||||||
|
# Health-check pont-bascule branché sur l'écran de pesée
|
||||||
|
|
||||||
|
**Date:** 2026-05-21
|
||||||
|
**Ticket:** FER-19
|
||||||
|
**Statut:** Spec validée
|
||||||
|
|
||||||
|
## Contexte
|
||||||
|
|
||||||
|
Aujourd'hui, à l'arrivée sur l'écran de pesée (réception ou expédition), l'UI affiche
|
||||||
|
un texte hardcodé « Pont-bascule connecté » et un loader. C'est du faux : l'état réel
|
||||||
|
du pont-bascule n'est jamais vérifié.
|
||||||
|
|
||||||
|
Le pont-bascule (Raspberry sur le réseau, ex. `http://100.122.43.54:8000`) expose deux routes :
|
||||||
|
|
||||||
|
- `POST /send/dsd` — déclenche une pesée et renvoie le poids + le DSD. Déjà utilisée via
|
||||||
|
`GET /receptions/weigh` et `GET /shipments/weigh` (state providers → `PontBasculeService::fetch()`).
|
||||||
|
- `GET /health` — health-check, **non branchée aujourd'hui**.
|
||||||
|
|
||||||
|
Réponse type de `/health` :
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"ok": true,
|
||||||
|
"mode": "serial",
|
||||||
|
"busy": false,
|
||||||
|
"hostname": "liot-rasp-ferme-01",
|
||||||
|
"timestamp": 1779357080.6277,
|
||||||
|
"port": "/dev/ttyUSB0",
|
||||||
|
"baudrate": 9600,
|
||||||
|
"port_connected": true,
|
||||||
|
"port_error": null
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Un bypass existe déjà côté backend (`PONT_BASCULE_BYPASS=true`) : il court-circuite l'appel
|
||||||
|
HTTP et renvoie un payload de test. Il est actif partout aujourd'hui (`.env.local`, `.env.prod`).
|
||||||
|
|
||||||
|
## Objectif
|
||||||
|
|
||||||
|
1. Brancher le vrai health-check du pont-bascule à l'arrivée sur l'écran de pesée.
|
||||||
|
2. Afficher le véritable état (connecté / non connecté) à la place du texte hardcodé.
|
||||||
|
3. Désactiver le bouton « peser » tant que le pont n'est pas valide.
|
||||||
|
4. Conserver le bypass : en bypass, l'état est « sain » pour ne pas casser le dev.
|
||||||
|
5. Ne pas dupliquer la configuration d'URL (une seule variable d'env de base).
|
||||||
|
|
||||||
|
## Décisions de design
|
||||||
|
|
||||||
|
- **Endpoint générique** `GET /pont_bascule/health` (ressource API Platform autonome).
|
||||||
|
Le health-check est agnostique de l'entité pesée — un seul endpoint partagé, pas un
|
||||||
|
par entité.
|
||||||
|
- **Check unique au montage** de l'écran de pesée (pas de polling). Si le pont est down
|
||||||
|
à l'arrivée, le bouton reste désactivé jusqu'au rechargement de la page. Conforme à la
|
||||||
|
demande.
|
||||||
|
- **Critère de validité** : `healthy = ok === true && port_connected === true && port_error === null && busy === false`.
|
||||||
|
(`busy` bloquant car une pesée déjà en cours met `busy` à `true`.)
|
||||||
|
- **Bypass** : `PONT_BASCULE_BYPASS=true` → health renvoie `healthy: true` sans appel réseau.
|
||||||
|
- **Pont injoignable = état normal** : le backend renvoie `200 { healthy: false }`
|
||||||
|
(jamais 500), pour ne pas déclencher de toast d'erreur côté `useApi`. `/weigh` garde
|
||||||
|
son comportement 500 actuel.
|
||||||
|
|
||||||
|
## Configuration env (suppression de la duplication)
|
||||||
|
|
||||||
|
- Remplacer `PONT_BASCULE_URL` (URL complète `.../send/dsd`) par **`PONT_BASCULE_BASE_URL`**
|
||||||
|
(URL de base sans chemin, ex. `http://100.122.43.54:8000`).
|
||||||
|
- Le service construit lui-même `{base}/send/dsd` et `{base}/health`.
|
||||||
|
- Fichiers à mettre à jour : `.env` (valeur vide), `.env.local`, `.env.prod`, et
|
||||||
|
`config/services.yaml` (argument `$baseUrl` au lieu de `$endpoint`).
|
||||||
|
- `PONT_BASCULE_BYPASS` inchangé.
|
||||||
|
|
||||||
|
## Backend
|
||||||
|
|
||||||
|
### `PontBasculeService` (`src/Service/PontBasculeService.php`)
|
||||||
|
|
||||||
|
- Renommer la dépendance `$endpoint` → `$baseUrl`.
|
||||||
|
- `fetch()` : POST sur `{baseUrl}/send/dsd` (comportement inchangé sinon).
|
||||||
|
- Nouvelle méthode `checkHealth(): PontBasculeHealth` :
|
||||||
|
- si `$bypass` → renvoie un `PontBasculeHealth` sain (`healthy = true`) sans appel réseau ;
|
||||||
|
- sinon → `GET {baseUrl}/health`, parse le JSON, calcule
|
||||||
|
`healthy = ok === true && port_connected === true && port_error === null && busy === false` ;
|
||||||
|
- si l'appel transport échoue **ou** le JSON est invalide / incomplet → renvoie
|
||||||
|
`PontBasculeHealth` avec `healthy = false` (aucune exception levée).
|
||||||
|
|
||||||
|
### DTO `PontBasculeHealth` (`src/Dto/PontBasculeHealth.php`)
|
||||||
|
|
||||||
|
Groupe de sérialisation `pont_bascule:health:read`. Champs :
|
||||||
|
|
||||||
|
- `healthy: bool` — le seul champ consommé par le front.
|
||||||
|
- Champs informatifs (debug / affichage futur) : `ok: bool`, `busy: bool`,
|
||||||
|
`portConnected: bool`, `portError: ?string`, `hostname: ?string`.
|
||||||
|
|
||||||
|
### Ressource API `PontBasculeHealthCheck` (`src/ApiResource/PontBasculeHealthCheck.php`)
|
||||||
|
|
||||||
|
- Classe carrier fine (vide) hébergeant l'opération `GET /pont_bascule/health`,
|
||||||
|
sans état persistant, `output` = `PontBasculeHealth::class` (le DTO), `provider` =
|
||||||
|
`PontBasculeHealthProvider`. Même montage que `/receptions/weigh` (host déclare
|
||||||
|
l'opération, `output` = DTO, provider renvoie le DTO).
|
||||||
|
- Nommée `PontBasculeHealthCheck` pour éviter la collision de nom court avec le DTO
|
||||||
|
`Dto\PontBasculeHealth`.
|
||||||
|
- Route conservée `/pont_bascule/health` (`pont_bascule` invariable).
|
||||||
|
|
||||||
|
### `PontBasculeHealthProvider` (`src/State/PontBasculeHealthProvider.php`)
|
||||||
|
|
||||||
|
- Appelle `PontBasculeService::checkHealth()` et renvoie le DTO `PontBasculeHealth`.
|
||||||
|
- Toujours `200`, même pont down (pas de `HttpException`).
|
||||||
|
|
||||||
|
## Frontend
|
||||||
|
|
||||||
|
### Couche service
|
||||||
|
|
||||||
|
- Nouveau composable **`usePontBascule`** (`composables/usePontBascule.ts`) exposant
|
||||||
|
`checkHealth()` → `api.get('pont_bascule/health')`, renvoie `{ healthy: boolean, ... }`.
|
||||||
|
Le health-check étant agnostique de l'entité, il vit dans son propre composable (et non
|
||||||
|
dans `workflow-service.ts` qui est un factory par entité) — une seule implémentation,
|
||||||
|
pas de duplication réception/expédition.
|
||||||
|
|
||||||
|
### `useWeighingStep.ts` (`composables/steps/`)
|
||||||
|
|
||||||
|
- Ajout d'un état réactif `pontBasculeStatus: 'checking' | 'connected' | 'disconnected'`
|
||||||
|
(initialisé à `'checking'`).
|
||||||
|
- Au `onMounted` : appel du health-check → `connected` si `healthy === true`, sinon `disconnected`.
|
||||||
|
- Exposer `pontBasculeStatus` au composant.
|
||||||
|
|
||||||
|
### `workflow-weight.vue` (`components/workflow/`)
|
||||||
|
|
||||||
|
- Le texte hardcodé ligne 5 (`Pont-bascule connecté`) devient dynamique selon
|
||||||
|
`pontBasculeStatus` :
|
||||||
|
- `checking` → « Vérification du pont-bascule… »
|
||||||
|
- `connected` → « Pont-bascule connecté » (vert, style actuel)
|
||||||
|
- `disconnected` → « Pont-bascule non connecté » (rouge)
|
||||||
|
- Le bouton **« peser »** reçoit `disabled` tant que `pontBasculeStatus !== 'connected'`
|
||||||
|
(état grisé). Les boutons « Valider la pesée » et « Générer le bon » restent inchangés.
|
||||||
|
|
||||||
|
### Textes d'état (codés en dur)
|
||||||
|
|
||||||
|
Convention du projet : les textes d'UI dans les composants/pages sont **codés en dur en
|
||||||
|
français** (le « Pont-bascule connecté » actuel l'est déjà). `fr.json` n'est consommé que
|
||||||
|
par `useApi` pour les toasts. On reste cohérent : les trois libellés sont écrits directement
|
||||||
|
dans `workflow-weight.vue`, **pas** dans `fr.json`.
|
||||||
|
|
||||||
|
- `checking` → « Vérification du pont-bascule… » (couleur primary)
|
||||||
|
- `connected` → « Pont-bascule connecté » (vert)
|
||||||
|
- `disconnected` → « Pont-bascule non connecté » (rouge)
|
||||||
|
|
||||||
|
## Error handling
|
||||||
|
|
||||||
|
- `/weigh` : comportement inchangé (500 + `PontBasculeException`).
|
||||||
|
- `/pont_bascule/health` : ne lève jamais d'erreur réseau vers le front. Pont injoignable
|
||||||
|
ou payload invalide = `200 { healthy: false }` → pas de toast d'erreur `useApi`.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
### Backend (PHPUnit, `MockHttpClient`)
|
||||||
|
|
||||||
|
Couvrir `PontBasculeService::checkHealth()` :
|
||||||
|
|
||||||
|
- bypass actif → `healthy = true`, aucun appel réseau ;
|
||||||
|
- payload sain (`ok`, `port_connected`, `port_error: null`, `busy: false`) → `healthy = true` ;
|
||||||
|
- `port_error` non null → `healthy = false` ;
|
||||||
|
- `port_connected: false` → `healthy = false` ;
|
||||||
|
- `busy: true` → `healthy = false` ;
|
||||||
|
- transport KO / JSON invalide → `healthy = false` (pas d'exception).
|
||||||
|
|
||||||
|
### Frontend
|
||||||
|
|
||||||
|
Pas de test auto existant pour ce flux. Validation manuelle :
|
||||||
|
|
||||||
|
- bypass on → bouton « peser » actif, texte « connecté » ;
|
||||||
|
- simuler un fail (`healthy: false`) → bouton grisé, texte « non connecté ».
|
||||||
|
|
||||||
|
## Hors périmètre
|
||||||
|
|
||||||
|
- Polling / rafraîchissement automatique de l'état (check unique au montage retenu).
|
||||||
|
- Bouton « réessayer » manuel.
|
||||||
|
- Traductions autres que `fr`.
|
||||||
@@ -2,7 +2,15 @@
|
|||||||
<div class="flex justify-center">
|
<div class="flex justify-center">
|
||||||
<div class="flex flex-col items-center w-[660px]">
|
<div class="flex flex-col items-center w-[660px]">
|
||||||
<h1 class="font-bold text-5xl uppercase text-primary-500">{{ title }}</h1>
|
<h1 class="font-bold text-5xl uppercase text-primary-500">{{ title }}</h1>
|
||||||
<p class="text-primary-500 uppercase text-2xl mt-2">Pont-bascule connecté</p>
|
<p
|
||||||
|
v-if="pontBasculeStatus === 'connected'"
|
||||||
|
class="uppercase text-2xl mt-2 text-green-600">Pont-bascule connecté</p>
|
||||||
|
<p
|
||||||
|
v-else-if="pontBasculeStatus === 'disconnected'"
|
||||||
|
class="uppercase text-2xl mt-2 text-red-600">Pont-bascule non connecté</p>
|
||||||
|
<p
|
||||||
|
v-else
|
||||||
|
class="uppercase text-2xl mt-2 invisible">Pont-bascule connecté</p>
|
||||||
<div
|
<div
|
||||||
v-if="!displayWeight"
|
v-if="!displayWeight"
|
||||||
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[86px]">
|
class="w-full flex flex-col items-center justify-center border border-black h-[90px] mt-12 mb-[86px]">
|
||||||
@@ -19,6 +27,7 @@
|
|||||||
<div class="flex justify-center mt-[54px]">
|
<div class="flex justify-center mt-[54px]">
|
||||||
<UiButton
|
<UiButton
|
||||||
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
class="text-xl uppercase bg-primary-500 text-white h-[50px] w-[272px]"
|
||||||
|
:disabled="pontBasculeStatus !== 'connected'"
|
||||||
@click="fetchWeight"
|
@click="fetchWeight"
|
||||||
>{{ displayWeight !== null ? 'refaire une pesée' : 'peser' }}</UiButton>
|
>{{ displayWeight !== null ? 'refaire une pesée' : 'peser' }}</UiButton>
|
||||||
<UiButton
|
<UiButton
|
||||||
@@ -35,7 +44,8 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { toRef } from 'vue'
|
import { onMounted, toRef } from 'vue'
|
||||||
|
import { usePontBascule } from '~/composables/usePontBascule'
|
||||||
import { useWeighingStep } from '~/composables/steps/useWeighingStep'
|
import { useWeighingStep } from '~/composables/steps/useWeighingStep'
|
||||||
import type { WeightData } from '~/services/dto/weight-data'
|
import type { WeightData } from '~/services/dto/weight-data'
|
||||||
|
|
||||||
@@ -55,6 +65,9 @@ const props = defineProps<{
|
|||||||
|
|
||||||
const entityRef = toRef(props, 'entity')
|
const entityRef = toRef(props, 'entity')
|
||||||
|
|
||||||
|
const { status: pontBasculeStatus, checkHealth } = usePontBascule()
|
||||||
|
onMounted(checkHealth)
|
||||||
|
|
||||||
const {
|
const {
|
||||||
displayWeight,
|
displayWeight,
|
||||||
title,
|
title,
|
||||||
|
|||||||
23
frontend/composables/usePontBascule.ts
Normal file
23
frontend/composables/usePontBascule.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { ref } from 'vue'
|
||||||
|
import { useApi } from '~/composables/useApi'
|
||||||
|
|
||||||
|
export type PontBasculeStatus = 'checking' | 'connected' | 'disconnected'
|
||||||
|
|
||||||
|
export const usePontBascule = () => {
|
||||||
|
const api = useApi()
|
||||||
|
const status = ref<PontBasculeStatus>('checking')
|
||||||
|
|
||||||
|
const checkHealth = async () => {
|
||||||
|
status.value = 'checking'
|
||||||
|
try {
|
||||||
|
const res = await api.get<{ healthy: boolean }>('pont_bascule/health', {}, {
|
||||||
|
toast: false
|
||||||
|
})
|
||||||
|
status.value = res.healthy ? 'connected' : 'disconnected'
|
||||||
|
} catch {
|
||||||
|
status.value = 'disconnected'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { status, checkHealth }
|
||||||
|
}
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<h1 class="text-3xl font-bold uppercase text-primary-500">bâtiments</h1>
|
<h1 class="text-3xl font-bold uppercase text-primary-500">bâtiments</h1>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-6 space-y-6">
|
<div class="mt-6 space-y-3">
|
||||||
<!-- Liste des bâtiments + rendu du plan de chaque bâtiment -->
|
<!-- Liste des bâtiments + rendu du plan de chaque bâtiment -->
|
||||||
<div
|
<div
|
||||||
v-for="entry in buildingLayouts"
|
v-for="entry in buildingLayouts"
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Légende : survol d'un statut => atténue les autres cases -->
|
<!-- Légende : survol d'un statut => atténue les autres cases -->
|
||||||
<div class="py-4">
|
<div class="">
|
||||||
<div class="flex gap-6">
|
<div class="flex gap-6">
|
||||||
<div
|
<div
|
||||||
v-for="statut in statutLegend"
|
v-for="statut in statutLegend"
|
||||||
|
|||||||
46
src/ApiResource/PontBasculeHealthCheck.php
Normal file
46
src/ApiResource/PontBasculeHealthCheck.php
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\ApiResource;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\ApiResource;
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use ApiPlatform\OpenApi\Model\Operation as OpenApiOperation;
|
||||||
|
use App\State\PontBasculeHealthProvider;
|
||||||
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
|
||||||
|
#[ApiResource(
|
||||||
|
operations: [
|
||||||
|
new Get(
|
||||||
|
uriTemplate: '/pont_bascule/health',
|
||||||
|
openapi: new OpenApiOperation(
|
||||||
|
summary: 'Pont-bascule health check',
|
||||||
|
description: 'Returns the connection state of the pont-bascule. Always 200, even when unreachable.',
|
||||||
|
),
|
||||||
|
normalizationContext: ['groups' => ['pont_bascule:health:read']],
|
||||||
|
security: "is_granted('ROLE_USER')",
|
||||||
|
provider: PontBasculeHealthProvider::class,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)]
|
||||||
|
final class PontBasculeHealthCheck
|
||||||
|
{
|
||||||
|
#[Groups(['pont_bascule:health:read'])]
|
||||||
|
public bool $healthy = false;
|
||||||
|
|
||||||
|
#[Groups(['pont_bascule:health:read'])]
|
||||||
|
public bool $ok = false;
|
||||||
|
|
||||||
|
#[Groups(['pont_bascule:health:read'])]
|
||||||
|
public bool $busy = false;
|
||||||
|
|
||||||
|
#[Groups(['pont_bascule:health:read'])]
|
||||||
|
public bool $portConnected = false;
|
||||||
|
|
||||||
|
#[Groups(['pont_bascule:health:read'])]
|
||||||
|
public ?string $portError = null;
|
||||||
|
|
||||||
|
#[Groups(['pont_bascule:health:read'])]
|
||||||
|
public ?string $hostname = null;
|
||||||
|
}
|
||||||
60
src/Dto/PontBasculeHealth.php
Normal file
60
src/Dto/PontBasculeHealth.php
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Dto;
|
||||||
|
|
||||||
|
use Symfony\Component\Serializer\Attribute\Groups;
|
||||||
|
|
||||||
|
final readonly class PontBasculeHealth
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
#[Groups(['pont_bascule:health:read'])]
|
||||||
|
private bool $healthy,
|
||||||
|
#[Groups(['pont_bascule:health:read'])]
|
||||||
|
private bool $ok = false,
|
||||||
|
#[Groups(['pont_bascule:health:read'])]
|
||||||
|
private bool $busy = false,
|
||||||
|
#[Groups(['pont_bascule:health:read'])]
|
||||||
|
private bool $portConnected = false,
|
||||||
|
#[Groups(['pont_bascule:health:read'])]
|
||||||
|
private ?string $portError = null,
|
||||||
|
#[Groups(['pont_bascule:health:read'])]
|
||||||
|
private ?string $hostname = null,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public static function unhealthy(): self
|
||||||
|
{
|
||||||
|
return new self(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isHealthy(): bool
|
||||||
|
{
|
||||||
|
return $this->healthy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isOk(): bool
|
||||||
|
{
|
||||||
|
return $this->ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isBusy(): bool
|
||||||
|
{
|
||||||
|
return $this->busy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isPortConnected(): bool
|
||||||
|
{
|
||||||
|
return $this->portConnected;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getPortError(): ?string
|
||||||
|
{
|
||||||
|
return $this->portError;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getHostname(): ?string
|
||||||
|
{
|
||||||
|
return $this->hostname;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace App\Service;
|
namespace App\Service;
|
||||||
|
|
||||||
|
use App\Dto\PontBasculeHealth;
|
||||||
use App\Dto\PontBasculeReading;
|
use App\Dto\PontBasculeReading;
|
||||||
use App\Exception\PontBasculeException;
|
use App\Exception\PontBasculeException;
|
||||||
use DateTimeImmutable;
|
use DateTimeImmutable;
|
||||||
@@ -15,7 +16,7 @@ final class PontBasculeService
|
|||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly HttpClientInterface $httpClient,
|
private readonly HttpClientInterface $httpClient,
|
||||||
private readonly PontBasculePayloadDecoder $payloadDecoder,
|
private readonly PontBasculePayloadDecoder $payloadDecoder,
|
||||||
private readonly string $endpoint,
|
private readonly string $baseUrl,
|
||||||
private readonly bool $bypass,
|
private readonly bool $bypass,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
@@ -28,7 +29,7 @@ final class PontBasculeService
|
|||||||
$body = $this->getBypassPayload();
|
$body = $this->getBypassPayload();
|
||||||
} else {
|
} else {
|
||||||
try {
|
try {
|
||||||
$response = $this->httpClient->request('POST', $this->endpoint);
|
$response = $this->httpClient->request('POST', $this->buildUrl('/send/dsd'));
|
||||||
$body = $response->getContent(false);
|
$body = $response->getContent(false);
|
||||||
} catch (TransportExceptionInterface $exception) {
|
} catch (TransportExceptionInterface $exception) {
|
||||||
throw PontBasculeException::transportFailure($exception->getMessage());
|
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
|
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"}';
|
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"}';
|
||||||
|
|||||||
32
src/State/PontBasculeHealthProvider.php
Normal file
32
src/State/PontBasculeHealthProvider.php
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\State;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\Operation;
|
||||||
|
use ApiPlatform\State\ProviderInterface;
|
||||||
|
use App\ApiResource\PontBasculeHealthCheck;
|
||||||
|
use App\Service\PontBasculeService;
|
||||||
|
|
||||||
|
final readonly class PontBasculeHealthProvider implements ProviderInterface
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private PontBasculeService $pontBasculeService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
public function provide(Operation $operation, array $uriVariables = [], array $context = []): PontBasculeHealthCheck
|
||||||
|
{
|
||||||
|
$health = $this->pontBasculeService->checkHealth();
|
||||||
|
|
||||||
|
$resource = new PontBasculeHealthCheck();
|
||||||
|
$resource->healthy = $health->isHealthy();
|
||||||
|
$resource->ok = $health->isOk();
|
||||||
|
$resource->busy = $health->isBusy();
|
||||||
|
$resource->portConnected = $health->isPortConnected();
|
||||||
|
$resource->portError = $health->getPortError();
|
||||||
|
$resource->hostname = $health->getHostname();
|
||||||
|
|
||||||
|
return $resource;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,7 +47,7 @@ final class PontBasculeServiceTest extends TestCase
|
|||||||
$httpClient
|
$httpClient
|
||||||
->expects(self::once())
|
->expects(self::once())
|
||||||
->method('request')
|
->method('request')
|
||||||
->with('POST', 'http://example.test')
|
->with('POST', 'http://example.test/send/dsd')
|
||||||
->willReturn($response)
|
->willReturn($response)
|
||||||
;
|
;
|
||||||
|
|
||||||
@@ -82,4 +82,103 @@ final class PontBasculeServiceTest extends TestCase
|
|||||||
|
|
||||||
$service->fetch();
|
$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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
78
tests/State/PontBasculeHealthProviderTest.php
Normal file
78
tests/State/PontBasculeHealthProviderTest.php
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace App\Tests\State;
|
||||||
|
|
||||||
|
use ApiPlatform\Metadata\Get;
|
||||||
|
use App\ApiResource\PontBasculeHealthCheck;
|
||||||
|
use App\Service\PontBasculePayloadDecoder;
|
||||||
|
use App\Service\PontBasculeService;
|
||||||
|
use App\State\PontBasculeHealthProvider;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
use Symfony\Contracts\HttpClient\HttpClientInterface;
|
||||||
|
use Symfony\Contracts\HttpClient\ResponseInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class PontBasculeHealthProviderTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testProvideMapsHealthyPayloadToResource(): void
|
||||||
|
{
|
||||||
|
$provider = $this->providerForHealthBody(json_encode([
|
||||||
|
'ok' => true,
|
||||||
|
'busy' => false,
|
||||||
|
'port_connected' => true,
|
||||||
|
'port_error' => null,
|
||||||
|
'hostname' => 'liot-rasp-ferme-01',
|
||||||
|
], JSON_THROW_ON_ERROR));
|
||||||
|
|
||||||
|
$result = $provider->provide(new Get());
|
||||||
|
|
||||||
|
self::assertInstanceOf(PontBasculeHealthCheck::class, $result);
|
||||||
|
self::assertTrue($result->healthy);
|
||||||
|
self::assertTrue($result->ok);
|
||||||
|
self::assertFalse($result->busy);
|
||||||
|
self::assertTrue($result->portConnected);
|
||||||
|
self::assertNull($result->portError);
|
||||||
|
self::assertSame('liot-rasp-ferme-01', $result->hostname);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testProvideMapsUnhealthyPayloadWithPortError(): void
|
||||||
|
{
|
||||||
|
$provider = $this->providerForHealthBody(json_encode([
|
||||||
|
'ok' => true,
|
||||||
|
'busy' => true,
|
||||||
|
'port_connected' => false,
|
||||||
|
'port_error' => 'device disconnected',
|
||||||
|
'hostname' => 'liot-rasp-ferme-01',
|
||||||
|
], JSON_THROW_ON_ERROR));
|
||||||
|
|
||||||
|
$result = $provider->provide(new Get());
|
||||||
|
|
||||||
|
self::assertFalse($result->healthy);
|
||||||
|
self::assertTrue($result->ok);
|
||||||
|
self::assertTrue($result->busy);
|
||||||
|
self::assertFalse($result->portConnected);
|
||||||
|
self::assertSame('device disconnected', $result->portError);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function providerForHealthBody(string $body): PontBasculeHealthProvider
|
||||||
|
{
|
||||||
|
$response = $this->createStub(ResponseInterface::class);
|
||||||
|
$response->method('getContent')->willReturn($body);
|
||||||
|
|
||||||
|
$httpClient = $this->createMock(HttpClientInterface::class);
|
||||||
|
$httpClient
|
||||||
|
->expects(self::once())
|
||||||
|
->method('request')
|
||||||
|
->with('GET', 'http://example.test/health')
|
||||||
|
->willReturn($response)
|
||||||
|
;
|
||||||
|
|
||||||
|
$service = new PontBasculeService($httpClient, new PontBasculePayloadDecoder(), 'http://example.test', false);
|
||||||
|
|
||||||
|
return new PontBasculeHealthProvider($service);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user