Files
home_automatization/laravel-app/tests/Feature/DeviceControllerTest.php
cacto be1f238f89 Перевод защищённой части приложения на Vue.js + Inertia
Дашборд, зоны, устройства, правила автоматизации и профиль теперь
Vue 3 + Inertia вместо Blade+Alpine — гостевые страницы Breeze
(логин/регистрация) сознательно оставлены на Blade, чтобы не трогать
уже протестированную аутентификацию. Контроллеры возвращают
Inertia::render() вместо view(), переиспользуют существующие
API Resources и FormRequest.

Ключевой нюанс: Resource/ResourceCollection, переданные напрямую в
проп Inertia, заворачиваются в ключ "data" (Inertia вызывает
toResponse() у Responsable-объектов) — везде используется ->resolve()
и явное резолвление вложенных ресурсов (zone/device_type внутри
Device и т.п.), иначе вложенные поля тоже задваивались бы обёрткой.

Alpine.js и все Blade-вьюхи защищённой части удалены как мёртвый код.
Тесты (ZoneControllerTest/DeviceControllerTest) переведены на
assertInertia(). UserFactory теперь явно задаёт role=owner — без
этого Eloquent не подтягивает DB-default обратно в модель после
create(), что уронило бы общий auth.user проп на любой странице.
2026-08-12 10:52:04 +05:00

134 lines
4.7 KiB
PHP

<?php
namespace Tests\Feature;
use App\Enums\DeviceCategory;
use App\Enums\UserRole;
use App\Models\Device;
use App\Models\DeviceType;
use App\Models\User;
use App\Models\Zone;
use App\Services\ClickHouseClient;
use App\Services\DeviceControlClient;
use App\Services\DeviceShadow;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Inertia\Testing\AssertableInertia as Assert;
use Tests\TestCase;
class DeviceControllerTest extends TestCase
{
use RefreshDatabase;
private function makeDevice(User $owner, array $capabilities): Device
{
$zone = Zone::forceCreate(['user_id' => $owner->id, 'name' => 'Гроубокс']);
$type = DeviceType::create([
'code' => 'fan',
'category' => DeviceCategory::Actuator,
'capabilities' => $capabilities,
]);
return Device::forceCreate([
'user_id' => $owner->id, 'zone_id' => $zone->id, 'device_type_id' => $type->id,
'name' => 'Вентилятор', 'external_id' => 'fan-1', 'protocol' => 'mqtt',
]);
}
public function test_owner_can_turn_on_device(): void
{
$owner = User::factory()->create(['role' => UserRole::Owner]);
$device = $this->makeDevice($owner, ['turn_on', 'turn_off']);
$this->mock(DeviceControlClient::class, function ($mock) {
$mock->shouldReceive('turnOn')->once()->with('fan-1')
->andReturn(['success' => true, 'error' => null]);
});
$response = $this->actingAs($owner)->post(route('devices.turn-on', $device));
$response->assertRedirect();
$response->assertSessionHas('status');
}
public function test_turn_on_rejected_when_device_lacks_capability(): void
{
$owner = User::factory()->create(['role' => UserRole::Owner]);
$device = $this->makeDevice($owner, ['turn_off']); // no turn_on
$this->mock(DeviceControlClient::class, function ($mock) {
$mock->shouldNotReceive('turnOn');
});
$response = $this->actingAs($owner)->post(route('devices.turn-on', $device));
$response->assertStatus(422);
}
public function test_viewer_cannot_turn_on_device(): void
{
$owner = User::factory()->create(['role' => UserRole::Owner]);
$viewer = User::factory()->create(['role' => UserRole::Viewer]);
$device = $this->makeDevice($owner, ['turn_on']);
$this->mock(DeviceControlClient::class, function ($mock) {
$mock->shouldNotReceive('turnOn');
});
$response = $this->actingAs($viewer)->post(route('devices.turn-on', $device));
$response->assertForbidden();
}
public function test_failed_command_flashes_error(): void
{
$owner = User::factory()->create(['role' => UserRole::Owner]);
$device = $this->makeDevice($owner, ['turn_on']);
$this->mock(DeviceControlClient::class, function ($mock) {
$mock->shouldReceive('turnOn')->once()
->andReturn(['success' => false, 'error' => 'device offline']);
});
$response = $this->actingAs($owner)->post(route('devices.turn-on', $device));
$response->assertSessionHas('error', 'device offline');
}
public function test_show_page_renders_shadow_and_telemetry(): void
{
$owner = User::factory()->create(['role' => UserRole::Owner]);
$zone = Zone::forceCreate(['user_id' => $owner->id, 'name' => 'Гроубокс']);
$type = DeviceType::create([
'code' => 'sensor_temp_humidity',
'category' => DeviceCategory::Sensor,
'capabilities' => ['temperature'],
]);
$device = Device::forceCreate([
'user_id' => $owner->id, 'zone_id' => $zone->id, 'device_type_id' => $type->id,
'name' => 'Датчик', 'external_id' => 'sensor-1', 'protocol' => 'mqtt',
]);
$this->mock(DeviceShadow::class, function ($mock) {
$mock->shouldReceive('snapshot')->once()->with('sensor-1')->andReturn([
'status' => 'online',
'last_seen' => null,
'desired_state' => [],
'reported_state' => [],
]);
});
$this->mock(ClickHouseClient::class, function ($mock) {
$mock->shouldReceive('query')->once()
->andReturn([['sensor_type' => 'temperature', 'value' => 24.5, 'recorded_at' => '2026-01-01 00:00:00']]);
});
$response = $this->actingAs($owner)->get(route('devices.show', $device));
$response->assertOk();
$response->assertInertia(fn (Assert $page) => $page
->component('Devices/Show')
->where('device.status', 'online')
->where('telemetry.0.value', 24.5)
);
}
}