Files
api/services/nginx/app/tests/Unit/Selfserve/GatewayShellyTransportTest.php
T
Jeppe Bundgaard c73e459d26 Add edge gateway broker configurations and session management routes
- Add broker-related configuration classes (`broker_url`, `public_broker_url`, `auth_mode`, `shared_secret`) to support edge gateway functionality.
- Enhance `SelfserveRoute` with routes for managing self-serve wash sessions, including session listing, detail retrieval, and forced lane stop.
- Update unit tests to validate new configuration handling, session routes, and OpenAPI endpoint coverage.
- Include default environment variables for broker settings in `docker-compose.example.yml`.
2026-04-28 10:04:17 +02:00

257 lines
8.8 KiB
PHP

<?php
app_require('classes/edge_gateway_manager.php');
app_require('classes/gateway_shelly_transport.php');
use classes\edge_gateway_manager;
use classes\gateway_shelly_transport;
class GatewayShellyTransportManagerFake extends edge_gateway_manager
{
/** @var array<int,array<string,mixed>> */
public array $statusCalls = [];
/** @var array<int,array<string,mixed>> */
public array $switchCalls = [];
/** @var array<int,array<string,mixed>> */
public array $localOnlyStatusCalls = [];
/** @var array<int,array<string,mixed>> */
public array $localOnlySwitchCalls = [];
public ?Exception $statusException = null;
public ?Exception $switchException = null;
public function __construct()
{
}
public function dispatchRelayStatus(int $departmentId, string $logicalRelayId, array $actionContext = []): array
{
if ($this->statusException instanceof Exception) {
throw $this->statusException;
}
$this->statusCalls[] = [
'department_id' => $departmentId,
'relay_id' => $logicalRelayId,
];
return [
'online' => true,
'on' => $logicalRelayId === 'relay-machine',
'binding' => [
'logical_relay_id' => $logicalRelayId,
'device_id' => 'device-' . $logicalRelayId,
'local_ip' => '192.168.1.50',
'channel' => 0,
],
'execution' => [
'transport' => 'gateway',
'fallback_mode' => 'PREFER_LOCAL',
],
'raw' => ['source' => 'status'],
];
}
public function dispatchRelaySwitch(int $departmentId, string $logicalRelayId, bool $on, array $actionContext = []): array
{
return $this->dispatchRelaySwitchWithTimer($departmentId, $logicalRelayId, $on, null, $actionContext);
}
public function dispatchRelaySwitchWithTimer(
int $departmentId,
string $logicalRelayId,
bool $on,
?int $toggleAfterSeconds,
array $actionContext = []
): array
{
if ($this->switchException instanceof Exception) {
throw $this->switchException;
}
$call = [
'department_id' => $departmentId,
'relay_id' => $logicalRelayId,
'on' => $on,
];
if ($toggleAfterSeconds !== null) {
$call['toggle_after'] = $toggleAfterSeconds;
}
$this->switchCalls[] = $call;
return [
'online' => true,
'on' => $on,
'raw' => ['source' => 'switch'],
];
}
public function dispatchRelayStatusLocalOnly(int $departmentId, string $logicalRelayId, array $actionContext = []): array
{
$this->localOnlyStatusCalls[] = [
'department_id' => $departmentId,
'relay_id' => $logicalRelayId,
];
return [
'online' => true,
'on' => true,
'raw' => ['source' => 'local-status'],
];
}
public function dispatchRelaySwitchLocalOnly(int $departmentId, string $logicalRelayId, bool $on, array $actionContext = []): array
{
return $this->dispatchRelaySwitchLocalOnlyWithTimer($departmentId, $logicalRelayId, $on, null, $actionContext);
}
public function dispatchRelaySwitchLocalOnlyWithTimer(
int $departmentId,
string $logicalRelayId,
bool $on,
?int $toggleAfterSeconds,
array $actionContext = []
): array
{
$call = [
'department_id' => $departmentId,
'relay_id' => $logicalRelayId,
'on' => $on,
];
if ($toggleAfterSeconds !== null) {
$call['toggle_after'] = $toggleAfterSeconds;
}
$this->localOnlySwitchCalls[] = $call;
return [
'online' => true,
'on' => $on,
'raw' => ['source' => 'local-switch'],
];
}
}
it('maps gateway relay status responses into the Shelly cloud payload shape', function (): void {
$manager = new GatewayShellyTransportManagerFake();
$transport = new gateway_shelly_transport($manager);
$result = $transport->sendPostRequest('/v2/devices/api/get', [
'ids' => ['relay-machine', 'relay-cleaner'],
], 17);
expect($manager->statusCalls)->toBe([
['department_id' => 17, 'relay_id' => 'relay-machine'],
['department_id' => 17, 'relay_id' => 'relay-cleaner'],
]);
expect($result)->toBeArray();
expect($result[0]['id'])->toBe('relay-machine');
expect($result[0]['relay_id'])->toBe('relay-machine');
expect($result[0]['status']['switch:0']['output'])->toBeTrue();
expect($result[0]['binding'])->toMatchArray([
'logical_relay_id' => 'relay-machine',
'device_id' => 'device-relay-machine',
'local_ip' => '192.168.1.50',
'channel' => 0,
]);
expect($result[0]['execution'])->toMatchArray([
'transport' => 'gateway',
'fallback_mode' => 'PREFER_LOCAL',
]);
expect($result[1]['status']['switch:0']['output'])->toBeFalse();
});
it('maps gateway relay switch responses into the Shelly cloud payload shape', function (): void {
$manager = new GatewayShellyTransportManagerFake();
$transport = new gateway_shelly_transport($manager);
$result = $transport->sendPostRequest('/v2/devices/api/set/switch', [
'id' => 'relay-machine',
'on' => false,
], 17);
expect($manager->switchCalls)->toBe([
['department_id' => 17, 'relay_id' => 'relay-machine', 'on' => false],
]);
expect($result)->toBeArray();
expect($result[0]['id'])->toBe('relay-machine');
expect($result[0]['status']['switch:0']['output'])->toBeFalse();
});
it('uses local-only gateway dispatch for explicit local transport overrides', function (): void {
$manager = new GatewayShellyTransportManagerFake();
$transport = new gateway_shelly_transport($manager, true);
$status = $transport->sendPostRequest('/v2/devices/api/get', [
'ids' => ['relay-entry'],
], 17);
$switch = $transport->sendPostRequest('/v2/devices/api/set/switch', [
'id' => 'relay-entry',
'on' => true,
], 17);
expect($manager->localOnlyStatusCalls)->toBe([
['department_id' => 17, 'relay_id' => 'relay-entry'],
]);
expect($manager->localOnlySwitchCalls)->toBe([
['department_id' => 17, 'relay_id' => 'relay-entry', 'on' => true],
]);
expect($manager->statusCalls)->toBe([]);
expect($manager->switchCalls)->toBe([]);
expect($status[0]['raw']['source'])->toBe('local-status');
expect($switch[0]['raw']['source'])->toBe('local-switch');
});
it('forwards Shelly toggle_after timers to gateway relay switches', function (): void {
$manager = new GatewayShellyTransportManagerFake();
$transport = new gateway_shelly_transport($manager, true);
$transport->sendPostRequest('/v2/devices/api/set/switch', [
'id' => 'relay-entry',
'on' => true,
'toggle_after' => 1,
], 17);
expect($manager->localOnlySwitchCalls)->toBe([
[
'department_id' => 17,
'relay_id' => 'relay-entry',
'on' => true,
'toggle_after' => 1,
],
]);
});
it('requires a valid department id for gateway transport requests', function (): void {
$transport = new gateway_shelly_transport(new GatewayShellyTransportManagerFake());
expect(fn() => $transport->sendPostRequest('/v2/devices/api/get', ['ids' => ['relay-machine']], null))
->toThrow(Exception::class, 'A department_id is required for gateway Shelly transport');
});
it('rejects unsupported gateway transport endpoints', function (): void {
$transport = new gateway_shelly_transport(new GatewayShellyTransportManagerFake());
expect(fn() => $transport->sendPostRequest('/v2/devices/api/unknown', [], 17))
->toThrow(Exception::class, 'Unsupported gateway Shelly transport endpoint');
});
it('surfaces binding lookup failures while resolving relay state through the gateway transport', function (): void {
$manager = new GatewayShellyTransportManagerFake();
$manager->statusException = new Exception('Relay binding missing');
$transport = new gateway_shelly_transport($manager);
expect(fn() => $transport->sendPostRequest('/v2/devices/api/get', [
'ids' => ['relay-machine'],
], 17))->toThrow(Exception::class, 'Relay binding missing');
});
it('surfaces offline gateway failures while dispatching relay switch commands', function (): void {
$manager = new GatewayShellyTransportManagerFake();
$manager->switchException = new Exception('Gateway agent is offline');
$transport = new gateway_shelly_transport($manager);
expect(fn() => $transport->sendPostRequest('/v2/devices/api/set/switch', [
'id' => 'relay-machine',
'on' => true,
], 17))->toThrow(Exception::class, 'Gateway agent is offline');
});