CRUD зон/устройств/правил автоматизации + RBAC owner/viewer
Policies (ZonePolicy/DevicePolicy/AutomationRulePolicy): не multi-tenant изоляция по user_id, а household-wide RBAC — все авторизованные видят одни и те же данные, только owner может создавать/менять/удалять. user_id на записи — это created_by, а не граница видимости. Form Requests с authorize() через политики; DeviceRequest учитывает unique external_id с ignore на update. AutomationRuleRequest: action_type остаётся строкой (не enum), т.к. это открытый список из device_types.capabilities; для действия set_level в форме есть отдельное поле "level" (Alpine.js переключает его видимость), которое контроллер мапит в action_params — без сырого JSON-редактора для MVP. Контроллеры без show (index+create/edit достаточно), Blade-формы на базе Breeze-компонентов. Навигация и дашборд (счётчики зон/устройств/активных правил) обновлены. Добавлен DemoGrowboxSeeder (демо-зона с датчиком, вентилятором и правилом) и viewer-пользователь в сидер. Восстановлен трейт AuthorizesRequests в базовом Controller (Laravel 11+ убрал его по умолчанию) — нужен для $this->authorize(). Проверено: 33/33 теста (включая новые ZoneControllerTest, AutomationRuleControllerTest — маппинг level→action_params, 403 для viewer), полный CRUD-цикл вживую через браузер под owner (создание зоны, правила с set_level, Alpine-переключение поля "Уровень"), доступ viewer подтверждён как read-only (нет кнопок изменения, прямой заход на /zones/create отдаёт 403).
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
<?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 Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Tests\TestCase;
|
||||
|
||||
class AutomationRuleControllerTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private function makeZoneWithDevices(User $owner): array
|
||||
{
|
||||
$zone = Zone::forceCreate(['user_id' => $owner->id, 'name' => 'Гроубокс']);
|
||||
|
||||
$sensorType = DeviceType::create([
|
||||
'code' => 'sensor_temp_humidity',
|
||||
'category' => DeviceCategory::Sensor,
|
||||
'capabilities' => ['temperature', 'humidity'],
|
||||
]);
|
||||
$fanType = DeviceType::create([
|
||||
'code' => 'fan',
|
||||
'category' => DeviceCategory::Actuator,
|
||||
'capabilities' => ['turn_on', 'turn_off', 'set_level'],
|
||||
]);
|
||||
|
||||
$sensor = Device::forceCreate([
|
||||
'user_id' => $owner->id, 'zone_id' => $zone->id, 'device_type_id' => $sensorType->id,
|
||||
'name' => 'Датчик', 'external_id' => 'sensor-1', 'protocol' => 'mqtt',
|
||||
]);
|
||||
$fan = Device::forceCreate([
|
||||
'user_id' => $owner->id, 'zone_id' => $zone->id, 'device_type_id' => $fanType->id,
|
||||
'name' => 'Вентилятор', 'external_id' => 'fan-1', 'protocol' => 'mqtt',
|
||||
]);
|
||||
|
||||
return [$zone, $sensor, $fan];
|
||||
}
|
||||
|
||||
public function test_owner_can_create_rule_with_turn_on_action(): void
|
||||
{
|
||||
$owner = User::factory()->create(['role' => UserRole::Owner]);
|
||||
[$zone, $sensor, $fan] = $this->makeZoneWithDevices($owner);
|
||||
|
||||
$response = $this->actingAs($owner)->post(route('automation-rules.store'), [
|
||||
'zone_id' => $zone->id,
|
||||
'condition_source_device_id' => $sensor->id,
|
||||
'condition_sensor_type' => 'temperature',
|
||||
'condition_operator' => '>',
|
||||
'condition_value' => 28,
|
||||
'target_device_id' => $fan->id,
|
||||
'action_type' => 'turn_on',
|
||||
'is_active' => '1',
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('automation-rules.index'));
|
||||
$this->assertDatabaseHas('automation_rules', [
|
||||
'condition_source_device_id' => $sensor->id,
|
||||
'target_device_id' => $fan->id,
|
||||
'action_type' => 'turn_on',
|
||||
'action_params' => '[]',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_set_level_action_requires_and_stores_level_param(): void
|
||||
{
|
||||
$owner = User::factory()->create(['role' => UserRole::Owner]);
|
||||
[$zone, $sensor, $fan] = $this->makeZoneWithDevices($owner);
|
||||
|
||||
// Missing level should fail validation.
|
||||
$missingLevel = $this->actingAs($owner)->post(route('automation-rules.store'), [
|
||||
'zone_id' => $zone->id,
|
||||
'condition_source_device_id' => $sensor->id,
|
||||
'condition_sensor_type' => 'temperature',
|
||||
'condition_operator' => '>',
|
||||
'condition_value' => 28,
|
||||
'target_device_id' => $fan->id,
|
||||
'action_type' => 'set_level',
|
||||
]);
|
||||
$missingLevel->assertSessionHasErrors('level');
|
||||
|
||||
$response = $this->actingAs($owner)->post(route('automation-rules.store'), [
|
||||
'zone_id' => $zone->id,
|
||||
'condition_source_device_id' => $sensor->id,
|
||||
'condition_sensor_type' => 'temperature',
|
||||
'condition_operator' => '>',
|
||||
'condition_value' => 28,
|
||||
'target_device_id' => $fan->id,
|
||||
'action_type' => 'set_level',
|
||||
'level' => 42,
|
||||
]);
|
||||
|
||||
$response->assertRedirect(route('automation-rules.index'));
|
||||
$this->assertDatabaseHas('automation_rules', [
|
||||
'target_device_id' => $fan->id,
|
||||
'action_type' => 'set_level',
|
||||
'action_params' => '{"level":42}',
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_viewer_cannot_create_rule(): void
|
||||
{
|
||||
$owner = User::factory()->create(['role' => UserRole::Owner]);
|
||||
$viewer = User::factory()->create(['role' => UserRole::Viewer]);
|
||||
[$zone, $sensor, $fan] = $this->makeZoneWithDevices($owner);
|
||||
|
||||
$response = $this->actingAs($viewer)->post(route('automation-rules.store'), [
|
||||
'zone_id' => $zone->id,
|
||||
'condition_source_device_id' => $sensor->id,
|
||||
'condition_sensor_type' => 'temperature',
|
||||
'condition_operator' => '>',
|
||||
'condition_value' => 28,
|
||||
'target_device_id' => $fan->id,
|
||||
'action_type' => 'turn_on',
|
||||
]);
|
||||
|
||||
$response->assertForbidden();
|
||||
$this->assertDatabaseCount('automation_rules', 0);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user