true, 'low_battery' => true, 'automation' => true]): void { Addon::create([ 'key' => 'smtp', 'installed' => true, 'status' => 'connected', 'config' => [ 'host' => 'smtp.example.com', 'port' => 587, 'encryption' => 'tls', 'username' => 'u', 'password' => 'p', 'from_email' => 'homeos@example.com', 'from_name' => 'HomeOS', 'to_email' => 'me@example.com', 'events' => $events, ], ]); } public function test_notify_does_nothing_without_the_smtp_addon(): void { Mail::fake(); $this->assertFalse(app(NotificationService::class)->notify('device_offline', 's', 'b')); } public function test_notify_respects_the_per_event_toggle(): void { Mail::fake(); $this->configureSmtp(['device_offline' => false, 'low_battery' => true, 'automation' => true]); $this->assertFalse(app(NotificationService::class)->notify('device_offline', 's', 'b')); $this->assertTrue(app(NotificationService::class)->notify('low_battery', 's', 'b')); } public function test_smtp_setup_persists_config_and_keeps_password(): void { $this->actingAs(User::factory()->create()); Livewire::test(SmtpSetup::class) ->set('host', 'smtp.gmail.com')->set('port', '587')->set('encryption', 'tls') ->set('username', 'me')->set('password', 'secret') ->set('fromEmail', 'from@x.com')->set('toEmail', 'to@x.com') ->set('evLowBattery', false) ->call('save') ->assertRedirect(route('addons.index')); $cfg = Addon::where('key', 'smtp')->first()->config; $this->assertSame('smtp.gmail.com', $cfg['host']); $this->assertSame('secret', $cfg['password']); $this->assertFalse($cfg['events']['low_battery']); // Re-saving with a blank password keeps the stored one. Livewire::test(SmtpSetup::class) ->set('host', 'smtp.gmail.com')->set('port', '465')->set('encryption', 'ssl') ->set('fromEmail', 'from@x.com')->set('toEmail', 'to@x.com') ->set('password', '') ->call('save'); $this->assertSame('secret', Addon::where('key', 'smtp')->first()->config['password']); } public function test_dead_battery_at_zero_percent_alerts(): void { Mail::fake(); $this->configureSmtp(); $device = \App\Models\Device::create(['name' => 'Sensor', 'vendor' => 'Shelly', 'protocol' => 'mqtt', 'status' => 'active', 'demo' => false, 'last_seen_at' => now(), 'config' => ['mqtt_prefix' => 'x']]); $entity = \App\Models\Entity::create(['device_id' => $device->id, 'type' => 'battery', 'key' => 'battery:0']); \App\Models\DeviceState::create(['entity_id' => $entity->id, 'state' => ['percent' => 0]]); $this->artisan('notifications:sweep')->assertSuccessful(); $this->assertTrue(Cache::has("notified:battery:{$entity->id}")); } public function test_smtp_setup_allows_no_encryption(): void { $this->actingAs(User::factory()->create()); Livewire::test(SmtpSetup::class) ->set('host', 'localhost')->set('port', '25')->set('encryption', '') ->set('fromEmail', 'a@b.com')->set('toEmail', 'c@d.com') ->call('save') ->assertHasNoErrors() ->assertRedirect(route('addons.index')); $this->assertSame('', Addon::where('key', 'smtp')->first()->config['encryption']); } public function test_sweep_emails_once_per_offline_device(): void { Mail::fake(); $this->configureSmtp(); // Active, not demo, stale last_seen → offline. $device = Device::create(['name' => 'Lampe', 'vendor' => 'Shelly', 'protocol' => 'http', 'status' => 'active', 'demo' => false, 'last_seen_at' => now()->subHour(), 'config' => ['ip' => '1.2.3.4']]); $this->artisan('notifications:sweep')->assertSuccessful(); $this->assertTrue(Cache::has("notified:offline:{$device->id}")); // Second sweep — still flagged, no re-notify (flag unchanged). $this->artisan('notifications:sweep')->assertSuccessful(); $this->assertTrue(Cache::has("notified:offline:{$device->id}")); // Back online → flag cleared so a future outage notifies again. $device->update(['last_seen_at' => now()]); $this->artisan('notifications:sweep')->assertSuccessful(); $this->assertFalse(Cache::has("notified:offline:{$device->id}")); } }