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