test: improve coverage

This commit is contained in:
Benjamin Nussbaum 2025-09-23 23:56:11 +02:00
parent 4f251bf37e
commit e9fc6b2335
7 changed files with 630 additions and 0 deletions

View file

@ -0,0 +1,40 @@
<?php
declare(strict_types=1);
use Database\Seeders\ExampleRecipesSeeder;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
test('example recipes seeder command calls seeder with correct user id', function () {
$seeder = Mockery::mock(ExampleRecipesSeeder::class);
$seeder->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);
});

View file

@ -0,0 +1,31 @@
<?php
declare(strict_types=1);
use App\Jobs\FirmwarePollJob;
use App\Models\Firmware;
use Illuminate\Foundation\Testing\RefreshDatabase;
uses(RefreshDatabase::class);
test('firmware check command has correct signature', function () {
$command = $this->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);
});

View file

@ -0,0 +1,140 @@
<?php
declare(strict_types=1);
use App\Jobs\NotifyDeviceBatteryLowJob;
use App\Models\Device;
use App\Models\User;
use App\Notifications\BatteryLow;
use Illuminate\Support\Facades\Notification;
test('it sends battery low notification when battery is below 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' => 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();
});

View file

@ -0,0 +1,119 @@
<?php
declare(strict_types=1);
use App\Models\DeviceModel;
test('device model has required attributes', function () {
$deviceModel = DeviceModel::factory()->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();
});

View file

@ -0,0 +1,77 @@
<?php
declare(strict_types=1);
use App\Models\Device;
use App\Models\User;
use App\Notifications\BatteryLow;
use App\Notifications\Channels\WebhookChannel;
use Illuminate\Notifications\Messages\MailMessage;
test('battery low notification has correct via channels', function () {
$device = Device::factory()->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,
]);
});

View file

@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
use App\Notifications\BatteryLow;
use App\Notifications\Channels\WebhookChannel;
use App\Notifications\Messages\WebhookMessage;
use App\Models\Device;
use App\Models\User;
use GuzzleHttp\Client;
use GuzzleHttp\Exception\GuzzleException;
use GuzzleHttp\Psr7\Response;
use Illuminate\Notifications\Notification;
test('webhook channel returns null when no webhook url is configured', function () {
$client = Mockery::mock(Client::class);
$channel = new WebhookChannel($client);
$user = new class extends User {
public function routeNotificationFor($driver, $notification = null)
{
return null; // No webhook URL configured
}
};
$notification = new BatteryLow(Device::factory()->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);
});

View file

@ -0,0 +1,92 @@
<?php
declare(strict_types=1);
use App\Notifications\Messages\WebhookMessage;
test('webhook message can be created with static method', function () {
$message = WebhookMessage::create('test data');
expect($message)->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();
});