119 lines
5.2 KiB
PHP
119 lines
5.2 KiB
PHP
<?php
|
|
|
|
namespace Tests\Feature;
|
|
|
|
use App\Models\Device;
|
|
use App\Models\DeviceState;
|
|
use App\Models\Entity;
|
|
use App\Services\DeviceCommandService;
|
|
use App\Services\ShellyLocalOnboarder;
|
|
use App\Services\ShellyPoller;
|
|
use App\Support\Shelly\ShellyRpc;
|
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Tests\TestCase;
|
|
|
|
class ShellyHttpTest extends TestCase
|
|
{
|
|
use RefreshDatabase;
|
|
|
|
private function fakeShelly(bool $switchOn = true): void
|
|
{
|
|
Http::fake(['*/rpc' => function ($request) use ($switchOn) {
|
|
$method = $request->data()['method'] ?? '';
|
|
$result = match ($method) {
|
|
'Shelly.GetDeviceInfo' => ['id' => 'shellyplus1-abc', 'model' => 'SNSW-001X16EU', 'name' => 'Flur'],
|
|
'Shelly.GetStatus' => [
|
|
'switch:0' => ['id' => 0, 'output' => $switchOn, 'apower' => 5.0],
|
|
'input:0' => ['id' => 0, 'state' => false],
|
|
'sys' => ['restart_required' => false],
|
|
],
|
|
default => [],
|
|
};
|
|
|
|
return Http::response(['id' => 1, 'result' => $result]);
|
|
}]);
|
|
}
|
|
|
|
public function test_local_onboarder_creates_an_http_device_from_the_api(): void
|
|
{
|
|
$this->fakeShelly();
|
|
|
|
$device = app(ShellyLocalOnboarder::class)->onboard('10.10.30.78', 'Deckenlicht', null);
|
|
|
|
$this->assertSame('http', $device->protocol);
|
|
$this->assertSame('10.10.30.78', data_get($device->config, 'ip'));
|
|
$this->assertSame('shellyplus1-abc', data_get($device->config, 'mqtt_prefix'));
|
|
$this->assertSame('SNSW-001X16EU', $device->model);
|
|
$this->assertTrue(data_get($device->entities()->where('key', 'switch:0')->first()->state->state, 'on'));
|
|
$this->assertSame(5, data_get($device->entities()->where('key', 'power:0')->first()->state->state, 'watts'));
|
|
}
|
|
|
|
public function test_onboarding_reuses_a_device_auto_created_via_mqtt(): void
|
|
{
|
|
$this->fakeShelly();
|
|
Device::create(['name' => 'x', 'vendor' => 'Shelly', 'protocol' => 'mqtt', 'config' => ['mqtt_prefix' => 'shellyplus1-abc']]);
|
|
|
|
app(ShellyLocalOnboarder::class)->onboard('10.10.30.78');
|
|
|
|
$this->assertSame(1, Device::where('config->mqtt_prefix', 'shellyplus1-abc')->count());
|
|
$this->assertSame('http', Device::where('config->mqtt_prefix', 'shellyplus1-abc')->first()->protocol);
|
|
}
|
|
|
|
public function test_onboarding_does_not_hijack_a_different_device_on_a_reused_ip(): void
|
|
{
|
|
// Device A previously held this IP (a different Shelly). After DHCP reassignment a new
|
|
// Shelly answers at the same IP — it must NOT overwrite A.
|
|
$a = Device::create(['name' => 'A', 'vendor' => 'Shelly', 'protocol' => 'http', 'config' => ['ip' => '10.10.30.78', 'mqtt_prefix' => 'other-shelly']]);
|
|
$this->fakeShelly(); // identifies as shellyplus1-abc
|
|
|
|
$device = app(ShellyLocalOnboarder::class)->onboard('10.10.30.78');
|
|
|
|
$this->assertNotSame($a->id, $device->id);
|
|
$this->assertSame('other-shelly', data_get($a->fresh()->config, 'mqtt_prefix'));
|
|
$this->assertSame('shellyplus1-abc', data_get($device->config, 'mqtt_prefix'));
|
|
}
|
|
|
|
public function test_http_driver_controls_via_rpc_and_audits(): void
|
|
{
|
|
$this->fakeShelly();
|
|
$device = app(ShellyLocalOnboarder::class)->onboard('10.10.30.78');
|
|
Http::fake(['*/rpc' => Http::response(['id' => 1, 'result' => []])]); // reset recorded calls target
|
|
|
|
$entity = $device->entities()->where('key', 'switch:0')->first();
|
|
$command = app(DeviceCommandService::class)->setOn($entity->load('device', 'state'), false);
|
|
|
|
$this->assertSame('ok', $command->result);
|
|
Http::assertSent(fn ($request) => str_contains($request->url(), '/rpc')
|
|
&& ($request->data()['method'] ?? '') === 'Switch.Set'
|
|
&& ($request->data()['params']['on'] ?? null) === false);
|
|
}
|
|
|
|
public function test_poller_applies_status_and_marks_reachable(): void
|
|
{
|
|
$device = Device::create(['name' => 'Flur', 'vendor' => 'Shelly', 'protocol' => 'http', 'config' => ['ip' => '10.10.30.78']]);
|
|
$this->fakeShelly(switchOn: false);
|
|
|
|
$this->assertTrue(app(ShellyPoller::class)->poll($device));
|
|
$this->assertFalse(data_get($device->entities()->where('key', 'switch:0')->first()->state->state, 'on'));
|
|
$this->assertNotNull($device->fresh()->last_seen_at);
|
|
}
|
|
|
|
public function test_poller_returns_false_when_unreachable(): void
|
|
{
|
|
$device = Device::create(['name' => 'Flur', 'vendor' => 'Shelly', 'protocol' => 'http', 'config' => ['ip' => '10.10.30.99']]);
|
|
Http::fake(['*/rpc' => fn () => throw new \Illuminate\Http\Client\ConnectionException('unreachable')]);
|
|
|
|
$this->assertFalse(app(ShellyPoller::class)->poll($device));
|
|
}
|
|
|
|
public function test_rpc_reachable_reflects_availability(): void
|
|
{
|
|
$this->fakeShelly();
|
|
$this->assertTrue(app(ShellyRpc::class)->reachable('10.10.30.78'));
|
|
|
|
Http::fake(['*/rpc' => fn () => throw new \Illuminate\Http\Client\ConnectionException('nope')]);
|
|
$this->assertFalse(app(ShellyRpc::class)->reachable('10.10.30.99'));
|
|
}
|
|
}
|