homeos/tests/Feature/RingIngestTest.php

118 lines
4.5 KiB
PHP

<?php
namespace Tests\Feature;
use App\Jobs\IngestRingMessage;
use App\Models\Addon;
use App\Models\Device;
use App\Support\Mqtt\RingNormalizer;
use App\Support\Mqtt\RingTopics;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Cache;
use Tests\TestCase;
class RingIngestTest extends TestCase
{
use RefreshDatabase;
private function installRing(): void
{
Addon::create(['key' => 'ring', 'installed' => true]);
Cache::flush();
}
public function test_topic_parser_reads_entity_topics(): void
{
$this->assertSame(
['location' => 'loc1', 'category' => 'camera', 'ring_id' => 'abc123', 'entity' => 'motion'],
RingTopics::parseState('ring/loc1/camera/abc123/motion/state'),
);
$this->assertTrue(RingTopics::isBridgeStatus('ring/loc1/status'));
$this->assertNull(RingTopics::parseState('ring/loc1/camera/abc123/motion/command'));
}
public function test_normalizer_maps_common_entities(): void
{
$this->assertSame(['on' => true], RingNormalizer::normalize('ding', 'ON')[0]['state']);
$this->assertSame(['on' => false], RingNormalizer::normalize('motion', 'OFF')[0]['state']);
$this->assertSame(['open' => true], RingNormalizer::normalize('contact', 'open')[0]['state']);
$this->assertSame(['percent' => 87], RingNormalizer::normalize('info', json_encode(['batteryLevel' => 87]))[0]['state']);
$this->assertSame([], RingNormalizer::normalize('unknown', 'ON'));
}
public function test_bridge_status_updates_addon(): void
{
$this->installRing();
(new IngestRingMessage('ring/loc1/status', 'online'))->handle();
$this->assertSame('connected', Addon::where('key', 'ring')->first()->status);
(new IngestRingMessage('ring/loc1/status', 'offline'))->handle();
$this->assertSame('error', Addon::where('key', 'ring')->first()->status);
}
public function test_doorbell_ding_auto_creates_cloud_device_and_state(): void
{
$this->installRing();
(new IngestRingMessage('ring/loc1/camera/frontdoor01/ding/state', 'ON'))->handle();
$device = Device::where('config->ring_id', 'frontdoor01')->first();
$this->assertNotNull($device);
$this->assertSame('Ring', $device->vendor);
$this->assertTrue($device->isCloud());
$ding = $device->entities()->where('key', 'ding')->first();
$this->assertTrue(data_get($ding->state->state, 'on'));
}
public function test_ingest_is_ignored_when_addon_not_installed(): void
{
Cache::flush(); // no ring addon row
(new IngestRingMessage('ring/loc1/camera/frontdoor01/motion/state', 'ON'))->handle();
$this->assertDatabaseCount('devices', 0);
}
public function test_unknown_entity_does_not_create_a_device(): void
{
$this->installRing();
(new IngestRingMessage('ring/loc1/camera/frontdoor01/wibble/state', 'ON'))->handle();
$this->assertDatabaseCount('devices', 0);
}
public function test_ring_id_is_unique_so_concurrent_creates_cannot_duplicate(): void
{
Device::create(['name' => 'A', 'vendor' => 'Ring', 'protocol' => 'mqtt', 'config' => ['ring_id' => 'dup1']]);
$this->expectException(\Illuminate\Database\UniqueConstraintViolationException::class);
Device::create(['name' => 'B', 'vendor' => 'Ring', 'protocol' => 'mqtt', 'config' => ['ring_id' => 'dup1']]);
}
public function test_repeated_messages_reuse_the_same_device(): void
{
$this->installRing();
(new IngestRingMessage('ring/loc1/camera/cam9/ding/state', 'ON'))->handle();
(new IngestRingMessage('ring/loc1/camera/cam9/motion/state', 'ON'))->handle();
(new IngestRingMessage('ring/loc1/camera/cam9/info/state', json_encode(['batteryLevel' => 50])))->handle();
$this->assertSame(1, Device::where('config->ring_id', 'cam9')->count());
$this->assertSame(3, Device::where('config->ring_id', 'cam9')->first()->entities()->count());
}
public function test_out_of_order_messages_do_not_overwrite_newer_state(): void
{
$this->installRing();
(new IngestRingMessage('ring/loc1/camera/cam1/motion/state', 'ON', 2_000))->handle();
(new IngestRingMessage('ring/loc1/camera/cam1/motion/state', 'OFF', 1_000))->handle();
$device = Device::where('config->ring_id', 'cam1')->first();
$this->assertTrue(data_get($device->entities()->where('key', 'motion')->first()->state->state, 'on'));
}
}