homeos/tests/Feature/ShellyHttpTest.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'));
}
}