From e9fc6b2335b2635cc6d27193ee2598cc652d0f12 Mon Sep 17 00:00:00 2001 From: Benjamin Nussbaum Date: Tue, 23 Sep 2025 23:56:11 +0200 Subject: [PATCH] test: improve coverage --- .../ExampleRecipesSeederCommandTest.php | 40 +++++ .../Console/FirmwareCheckCommandTest.php | 31 ++++ .../Jobs/NotifyDeviceBatteryLowJobTest.php | 140 ++++++++++++++++++ tests/Unit/Models/DeviceModelTest.php | 119 +++++++++++++++ tests/Unit/Notifications/BatteryLowTest.php | 77 ++++++++++ .../Unit/Notifications/WebhookChannelTest.php | 131 ++++++++++++++++ .../Unit/Notifications/WebhookMessageTest.php | 92 ++++++++++++ 7 files changed, 630 insertions(+) create mode 100644 tests/Feature/Console/ExampleRecipesSeederCommandTest.php create mode 100644 tests/Feature/Console/FirmwareCheckCommandTest.php create mode 100644 tests/Feature/Jobs/NotifyDeviceBatteryLowJobTest.php create mode 100644 tests/Unit/Models/DeviceModelTest.php create mode 100644 tests/Unit/Notifications/BatteryLowTest.php create mode 100644 tests/Unit/Notifications/WebhookChannelTest.php create mode 100644 tests/Unit/Notifications/WebhookMessageTest.php diff --git a/tests/Feature/Console/ExampleRecipesSeederCommandTest.php b/tests/Feature/Console/ExampleRecipesSeederCommandTest.php new file mode 100644 index 0000000..e6b2411 --- /dev/null +++ b/tests/Feature/Console/ExampleRecipesSeederCommandTest.php @@ -0,0 +1,40 @@ +shouldReceive('run') + ->once() + ->with('123'); + + $this->app->instance(ExampleRecipesSeeder::class, $seeder); + + $this->artisan('recipes:seed', ['user_id' => '123']) + ->assertExitCode(0); +}); + +test('example recipes seeder command has correct signature', function () { + $command = $this->app->make(\App\Console\Commands\ExampleRecipesSeederCommand::class); + + expect($command->getName())->toBe('recipes:seed'); + expect($command->getDescription())->toBe('Seed example recipes'); +}); + +test('example recipes seeder command prompts for missing input', function () { + $seeder = Mockery::mock(ExampleRecipesSeeder::class); + $seeder->shouldReceive('run') + ->once() + ->with('456'); + + $this->app->instance(ExampleRecipesSeeder::class, $seeder); + + $this->artisan('recipes:seed') + ->expectsQuestion('What is the user_id?', '456') + ->assertExitCode(0); +}); diff --git a/tests/Feature/Console/FirmwareCheckCommandTest.php b/tests/Feature/Console/FirmwareCheckCommandTest.php new file mode 100644 index 0000000..bed56d1 --- /dev/null +++ b/tests/Feature/Console/FirmwareCheckCommandTest.php @@ -0,0 +1,31 @@ +app->make(\App\Console\Commands\FirmwareCheckCommand::class); + + expect($command->getName())->toBe('trmnl:firmware:check'); + expect($command->getDescription())->toBe('Checks for the latest firmware and downloads it if flag --download is passed.'); +}); + +test('firmware check command runs without errors', function () { + $this->artisan('trmnl:firmware:check') + ->assertExitCode(0); +}); + +test('firmware check command runs with download flag', function () { + $this->artisan('trmnl:firmware:check', ['--download' => true]) + ->assertExitCode(0); +}); + +test('firmware check command can run successfully', function () { + $this->artisan('trmnl:firmware:check') + ->assertExitCode(0); +}); diff --git a/tests/Feature/Jobs/NotifyDeviceBatteryLowJobTest.php b/tests/Feature/Jobs/NotifyDeviceBatteryLowJobTest.php new file mode 100644 index 0000000..c718ef1 --- /dev/null +++ b/tests/Feature/Jobs/NotifyDeviceBatteryLowJobTest.php @@ -0,0 +1,140 @@ + 20]); + + $user = User::factory()->create(); + $device = Device::factory()->create([ + 'user_id' => $user->id, + 'last_battery_voltage' => 3.0, // This should result in low battery percentage + 'battery_notification_sent' => false, + ]); + + $job = new NotifyDeviceBatteryLowJob(); + $job->handle(); + + Notification::assertSentTo($user, BatteryLow::class); + + $device->refresh(); + expect($device->battery_notification_sent)->toBeTrue(); +}); + +test('it does not send notification when battery is above threshold', function () { + Notification::fake(); + + config(['app.notifications.battery_low.warn_at_percent' => 20]); + + $user = User::factory()->create(); + $device = Device::factory()->create([ + 'user_id' => $user->id, + 'last_battery_voltage' => 4.0, // This should result in high battery percentage + 'battery_notification_sent' => false, + ]); + + $job = new NotifyDeviceBatteryLowJob(); + $job->handle(); + + Notification::assertNotSentTo($user, BatteryLow::class); + + $device->refresh(); + expect($device->battery_notification_sent)->toBeFalse(); +}); + +test('it does not send notification when already sent', function () { + Notification::fake(); + + config(['app.notifications.battery_low.warn_at_percent' => 20]); + + $user = User::factory()->create(); + $device = Device::factory()->create([ + 'user_id' => $user->id, + 'last_battery_voltage' => 3.0, // Low battery + 'battery_notification_sent' => true, // Already sent + ]); + + $job = new NotifyDeviceBatteryLowJob(); + $job->handle(); + + Notification::assertNotSentTo($user, BatteryLow::class); +}); + +test('it resets notification flag when battery is above threshold', function () { + Notification::fake(); + + config(['app.notifications.battery_low.warn_at_percent' => 20]); + + $user = User::factory()->create(); + $device = Device::factory()->create([ + 'user_id' => $user->id, + 'last_battery_voltage' => 4.0, // High battery + 'battery_notification_sent' => true, // Was previously sent + ]); + + $job = new NotifyDeviceBatteryLowJob(); + $job->handle(); + + Notification::assertNotSentTo($user, BatteryLow::class); + + $device->refresh(); + expect($device->battery_notification_sent)->toBeFalse(); +}); + +test('it skips devices without associated user', function () { + Notification::fake(); + + config(['app.notifications.battery_low.warn_at_percent' => 20]); + + $device = Device::factory()->create([ + 'user_id' => null, + 'last_battery_voltage' => 3.0, // Low battery + 'battery_notification_sent' => false, + ]); + + $job = new NotifyDeviceBatteryLowJob(); + $job->handle(); + + Notification::assertNothingSent(); +}); + +test('it processes multiple devices correctly', function () { + Notification::fake(); + + config(['app.notifications.battery_low.warn_at_percent' => 20]); + + $user1 = User::factory()->create(); + $user2 = User::factory()->create(); + + $device1 = Device::factory()->create([ + 'user_id' => $user1->id, + 'last_battery_voltage' => 3.0, // Low battery + 'battery_notification_sent' => false, + ]); + + $device2 = Device::factory()->create([ + 'user_id' => $user2->id, + 'last_battery_voltage' => 4.0, // High battery + 'battery_notification_sent' => false, + ]); + + $job = new NotifyDeviceBatteryLowJob(); + $job->handle(); + + Notification::assertSentTo($user1, BatteryLow::class); + Notification::assertNotSentTo($user2, BatteryLow::class); + + $device1->refresh(); + $device2->refresh(); + + expect($device1->battery_notification_sent)->toBeTrue(); + expect($device2->battery_notification_sent)->toBeFalse(); +}); diff --git a/tests/Unit/Models/DeviceModelTest.php b/tests/Unit/Models/DeviceModelTest.php new file mode 100644 index 0000000..24904d6 --- /dev/null +++ b/tests/Unit/Models/DeviceModelTest.php @@ -0,0 +1,119 @@ +create([ + 'name' => 'Test Model', + 'width' => 800, + 'height' => 480, + 'colors' => 4, + 'bit_depth' => 2, + 'scale_factor' => 1.0, + 'rotation' => 0, + 'offset_x' => 0, + 'offset_y' => 0, + ]); + + expect($deviceModel->name)->toBe('Test Model'); + expect($deviceModel->width)->toBe(800); + expect($deviceModel->height)->toBe(480); + expect($deviceModel->colors)->toBe(4); + expect($deviceModel->bit_depth)->toBe(2); + expect($deviceModel->scale_factor)->toBe(1.0); + expect($deviceModel->rotation)->toBe(0); + expect($deviceModel->offset_x)->toBe(0); + expect($deviceModel->offset_y)->toBe(0); +}); + +test('device model casts attributes correctly', function () { + $deviceModel = DeviceModel::factory()->create([ + 'width' => '800', + 'height' => '480', + 'colors' => '4', + 'bit_depth' => '2', + 'scale_factor' => '1.5', + 'rotation' => '90', + 'offset_x' => '10', + 'offset_y' => '20', + ]); + + expect($deviceModel->width)->toBeInt(); + expect($deviceModel->height)->toBeInt(); + expect($deviceModel->colors)->toBeInt(); + expect($deviceModel->bit_depth)->toBeInt(); + expect($deviceModel->scale_factor)->toBeFloat(); + expect($deviceModel->rotation)->toBeInt(); + expect($deviceModel->offset_x)->toBeInt(); + expect($deviceModel->offset_y)->toBeInt(); +}); + +test('get color depth attribute returns correct format for bit depth 2', function () { + $deviceModel = DeviceModel::factory()->create(['bit_depth' => 2]); + + expect($deviceModel->getColorDepthAttribute())->toBe('2bit'); +}); + +test('get color depth attribute returns correct format for bit depth 4', function () { + $deviceModel = DeviceModel::factory()->create(['bit_depth' => 4]); + + expect($deviceModel->getColorDepthAttribute())->toBe('4bit'); +}); + +test('get color depth attribute returns 4bit for bit depth greater than 4', function () { + $deviceModel = DeviceModel::factory()->create(['bit_depth' => 8]); + + expect($deviceModel->getColorDepthAttribute())->toBe('4bit'); +}); + +test('get color depth attribute returns null when bit depth is null', function () { + $deviceModel = new DeviceModel(['bit_depth' => null]); + + expect($deviceModel->getColorDepthAttribute())->toBeNull(); +}); + +test('get scale level attribute returns null for width 800 or less', function () { + $deviceModel = DeviceModel::factory()->create(['width' => 800]); + + expect($deviceModel->getScaleLevelAttribute())->toBeNull(); +}); + +test('get scale level attribute returns large for width between 801 and 1000', function () { + $deviceModel = DeviceModel::factory()->create(['width' => 900]); + + expect($deviceModel->getScaleLevelAttribute())->toBe('large'); +}); + +test('get scale level attribute returns xlarge for width between 1001 and 1400', function () { + $deviceModel = DeviceModel::factory()->create(['width' => 1200]); + + expect($deviceModel->getScaleLevelAttribute())->toBe('xlarge'); +}); + +test('get scale level attribute returns xxlarge for width greater than 1400', function () { + $deviceModel = DeviceModel::factory()->create(['width' => 1500]); + + expect($deviceModel->getScaleLevelAttribute())->toBe('xxlarge'); +}); + +test('get scale level attribute returns null when width is null', function () { + $deviceModel = new DeviceModel(['width' => null]); + + expect($deviceModel->getScaleLevelAttribute())->toBeNull(); +}); + +test('device model factory creates valid data', function () { + $deviceModel = DeviceModel::factory()->create(); + + expect($deviceModel->name)->not->toBeEmpty(); + expect($deviceModel->width)->toBeInt(); + expect($deviceModel->height)->toBeInt(); + expect($deviceModel->colors)->toBeInt(); + expect($deviceModel->bit_depth)->toBeInt(); + expect($deviceModel->scale_factor)->toBeFloat(); + expect($deviceModel->rotation)->toBeInt(); + expect($deviceModel->offset_x)->toBeInt(); + expect($deviceModel->offset_y)->toBeInt(); +}); diff --git a/tests/Unit/Notifications/BatteryLowTest.php b/tests/Unit/Notifications/BatteryLowTest.php new file mode 100644 index 0000000..061ef94 --- /dev/null +++ b/tests/Unit/Notifications/BatteryLowTest.php @@ -0,0 +1,77 @@ +create(); + $notification = new BatteryLow($device); + + expect($notification->via(new User()))->toBe(['mail', WebhookChannel::class]); +}); + +test('battery low notification creates correct mail message', function () { + $device = Device::factory()->create([ + 'name' => 'Test Device', + 'last_battery_voltage' => 3.0, + ]); + + $notification = new BatteryLow($device); + $mailMessage = $notification->toMail(new User()); + + expect($mailMessage)->toBeInstanceOf(MailMessage::class); + expect($mailMessage->markdown)->toBe('mail.battery-low'); + expect($mailMessage->viewData['device'])->toBe($device); +}); + +test('battery low notification creates correct webhook message', function () { + config([ + 'services.webhook.notifications.topic' => 'battery.low', + 'app.name' => 'Test App', + ]); + + $device = Device::factory()->create([ + 'name' => 'Test Device', + 'last_battery_voltage' => 3.0, + ]); + + $notification = new BatteryLow($device); + $webhookMessage = $notification->toWebhook(new User()); + + expect($webhookMessage->toArray())->toBe([ + 'query' => null, + 'data' => [ + 'topic' => 'battery.low', + 'message' => "Battery below {$device->battery_percent}% on device: Test Device", + 'device_id' => $device->id, + 'device_name' => 'Test Device', + 'battery_percent' => $device->battery_percent, + ], + 'headers' => [ + 'User-Agent' => 'Test App', + 'X-TrmnlByos-Event' => 'battery.low', + ], + 'verify' => false, + ]); +}); + +test('battery low notification creates correct array representation', function () { + $device = Device::factory()->create([ + 'name' => 'Test Device', + 'last_battery_voltage' => 3.0, + ]); + + $notification = new BatteryLow($device); + $array = $notification->toArray(new User()); + + expect($array)->toBe([ + 'device_name' => 'Test Device', + 'battery_percent' => $device->battery_percent, + ]); +}); + diff --git a/tests/Unit/Notifications/WebhookChannelTest.php b/tests/Unit/Notifications/WebhookChannelTest.php new file mode 100644 index 0000000..8c637ab --- /dev/null +++ b/tests/Unit/Notifications/WebhookChannelTest.php @@ -0,0 +1,131 @@ +create()); + + $result = $channel->send($user, $notification); + + expect($result)->toBeNull(); +}); + +test('webhook channel throws exception when notification does not implement toWebhook', function () { + $client = Mockery::mock(Client::class); + $channel = new WebhookChannel($client); + + $user = new class extends User { + public function routeNotificationFor($driver, $notification = null) + { + return 'https://example.com/webhook'; + } + }; + + $notification = new class extends Notification { + public function via($notifiable) + { + return []; + } + }; + + expect(fn() => $channel->send($user, $notification)) + ->toThrow(\Exception::class, 'Notification does not implement toWebhook method.'); +}); + +test('webhook channel sends successful webhook request', function () { + $client = Mockery::mock(Client::class); + $channel = new WebhookChannel($client); + + $user = new class extends User { + public function routeNotificationFor($driver, $notification = null) + { + return 'https://example.com/webhook'; + } + }; + + $device = Device::factory()->create(); + $notification = new BatteryLow($device); + + $expectedResponse = new Response(200, [], 'OK'); + + $client->shouldReceive('post') + ->once() + ->with('https://example.com/webhook', [ + 'query' => null, + 'body' => json_encode($notification->toWebhook($user)->toArray()['data']), + 'verify' => false, + 'headers' => $notification->toWebhook($user)->toArray()['headers'], + ]) + ->andReturn($expectedResponse); + + $result = $channel->send($user, $notification); + + expect($result)->toBe($expectedResponse); +}); + + +test('webhook channel throws exception when response status is not successful', function () { + $client = Mockery::mock(Client::class); + $channel = new WebhookChannel($client); + + $user = new class extends User { + public function routeNotificationFor($driver, $notification = null) + { + return 'https://example.com/webhook'; + } + }; + + $device = Device::factory()->create(); + $notification = new BatteryLow($device); + + $errorResponse = new Response(400, [], 'Bad Request'); + + $client->shouldReceive('post') + ->once() + ->andReturn($errorResponse); + + expect(fn() => $channel->send($user, $notification)) + ->toThrow(\Exception::class, 'Webhook request failed with status code: 400'); +}); + +test('webhook channel handles guzzle exceptions', function () { + $client = Mockery::mock(Client::class); + $channel = new WebhookChannel($client); + + $user = new class extends User { + public function routeNotificationFor($driver, $notification = null) + { + return 'https://example.com/webhook'; + } + }; + + $device = Device::factory()->create(); + $notification = new BatteryLow($device); + + $client->shouldReceive('post') + ->once() + ->andThrow(new class extends \Exception implements GuzzleException {}); + + expect(fn() => $channel->send($user, $notification)) + ->toThrow(\Exception::class); +}); diff --git a/tests/Unit/Notifications/WebhookMessageTest.php b/tests/Unit/Notifications/WebhookMessageTest.php new file mode 100644 index 0000000..0f9ef3e --- /dev/null +++ b/tests/Unit/Notifications/WebhookMessageTest.php @@ -0,0 +1,92 @@ +toBeInstanceOf(WebhookMessage::class); +}); + +test('webhook message can be created with constructor', function () { + $message = new WebhookMessage('test data'); + + expect($message)->toBeInstanceOf(WebhookMessage::class); +}); + +test('webhook message can set query parameters', function () { + $message = WebhookMessage::create() + ->query(['param1' => 'value1', 'param2' => 'value2']); + + expect($message->toArray()['query'])->toBe(['param1' => 'value1', 'param2' => 'value2']); +}); + +test('webhook message can set data', function () { + $data = ['key' => 'value', 'nested' => ['array' => 'data']]; + $message = WebhookMessage::create() + ->data($data); + + expect($message->toArray()['data'])->toBe($data); +}); + +test('webhook message can add headers', function () { + $message = WebhookMessage::create() + ->header('X-Custom-Header', 'custom-value') + ->header('Authorization', 'Bearer token'); + + $headers = $message->toArray()['headers']; + expect($headers['X-Custom-Header'])->toBe('custom-value'); + expect($headers['Authorization'])->toBe('Bearer token'); +}); + +test('webhook message can set user agent', function () { + $message = WebhookMessage::create() + ->userAgent('Test App/1.0'); + + $headers = $message->toArray()['headers']; + expect($headers['User-Agent'])->toBe('Test App/1.0'); +}); + +test('webhook message can set verify option', function () { + $message = WebhookMessage::create() + ->verify(true); + + expect($message->toArray()['verify'])->toBeTrue(); +}); + +test('webhook message verify defaults to false', function () { + $message = WebhookMessage::create(); + + expect($message->toArray()['verify'])->toBeFalse(); +}); + +test('webhook message can chain methods', function () { + $message = WebhookMessage::create(['initial' => 'data']) + ->query(['param' => 'value']) + ->data(['updated' => 'data']) + ->header('X-Test', 'header') + ->userAgent('Test Agent') + ->verify(true); + + $array = $message->toArray(); + + expect($array['query'])->toBe(['param' => 'value']); + expect($array['data'])->toBe(['updated' => 'data']); + expect($array['headers']['X-Test'])->toBe('header'); + expect($array['headers']['User-Agent'])->toBe('Test Agent'); + expect($array['verify'])->toBeTrue(); +}); + +test('webhook message toArray returns correct structure', function () { + $message = WebhookMessage::create(['test' => 'data']); + + $array = $message->toArray(); + + expect($array)->toHaveKeys(['query', 'data', 'headers', 'verify']); + expect($array['query'])->toBeNull(); + expect($array['data'])->toBe(['test' => 'data']); + expect($array['headers'])->toBeNull(); + expect($array['verify'])->toBeFalse(); +});