$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) ); } }