test: improve coverage
Some checks are pending
tests / ci (push) Waiting to run

This commit is contained in:
Benjamin Nussbaum 2025-09-24 00:45:50 +02:00
parent e9fc6b2335
commit 5d3a512203
20 changed files with 1651 additions and 101 deletions

View file

@ -20,7 +20,7 @@ test('example recipes seeder command calls seeder with correct user id', functio
});
test('example recipes seeder command has correct signature', function () {
$command = $this->app->make(\App\Console\Commands\ExampleRecipesSeederCommand::class);
$command = $this->app->make(App\Console\Commands\ExampleRecipesSeederCommand::class);
expect($command->getName())->toBe('recipes:seed');
expect($command->getDescription())->toBe('Seed example recipes');

View file

@ -2,14 +2,12 @@
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);
$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.');

View file

@ -0,0 +1,86 @@
<?php
declare(strict_types=1);
use App\Models\Device;
use App\Models\Firmware;
use App\Models\User;
test('firmware update command has correct signature', function () {
$this->artisan('trmnl:firmware:update --help')
->assertExitCode(0);
});
test('firmware update command can be called', function () {
$user = User::factory()->create();
$device = Device::factory()->create(['user_id' => $user->id]);
$firmware = Firmware::factory()->create(['version_tag' => '1.0.0']);
$this->artisan('trmnl:firmware:update')
->expectsQuestion('Check for new firmware?', 'no')
->expectsQuestion('Update to which version?', $firmware->id)
->expectsQuestion('Which devices should be updated?', ["_$device->id"])
->assertExitCode(0);
$device->refresh();
expect($device->update_firmware_id)->toBe($firmware->id);
});
test('firmware update command updates all devices when all is selected', function () {
$user = User::factory()->create();
$device1 = Device::factory()->create(['user_id' => $user->id]);
$device2 = Device::factory()->create(['user_id' => $user->id]);
$firmware = Firmware::factory()->create(['version_tag' => '1.0.0']);
$this->artisan('trmnl:firmware:update')
->expectsQuestion('Check for new firmware?', 'no')
->expectsQuestion('Update to which version?', $firmware->id)
->expectsQuestion('Which devices should be updated?', ['all'])
->assertExitCode(0);
$device1->refresh();
$device2->refresh();
expect($device1->update_firmware_id)->toBe($firmware->id);
expect($device2->update_firmware_id)->toBe($firmware->id);
});
test('firmware update command aborts when no devices selected', function () {
$firmware = Firmware::factory()->create(['version_tag' => '1.0.0']);
$this->artisan('trmnl:firmware:update')
->expectsQuestion('Check for new firmware?', 'no')
->expectsQuestion('Update to which version?', $firmware->id)
->expectsQuestion('Which devices should be updated?', [])
->expectsOutput('No devices selected. Aborting.')
->assertExitCode(0);
});
test('firmware update command calls firmware check when check is selected', function () {
$user = User::factory()->create();
$device = Device::factory()->create(['user_id' => $user->id]);
$firmware = Firmware::factory()->create(['version_tag' => '1.0.0']);
$this->artisan('trmnl:firmware:update')
->expectsQuestion('Check for new firmware?', 'check')
->expectsQuestion('Update to which version?', $firmware->id)
->expectsQuestion('Which devices should be updated?', ["_$device->id"])
->assertExitCode(0);
$device->refresh();
expect($device->update_firmware_id)->toBe($firmware->id);
});
test('firmware update command calls firmware check with download when download is selected', function () {
$user = User::factory()->create();
$device = Device::factory()->create(['user_id' => $user->id]);
$firmware = Firmware::factory()->create(['version_tag' => '1.0.0']);
$this->artisan('trmnl:firmware:update')
->expectsQuestion('Check for new firmware?', 'download')
->expectsQuestion('Update to which version?', $firmware->id)
->expectsQuestion('Which devices should be updated?', ["_$device->id"])
->assertExitCode(0);
$device->refresh();
expect($device->update_firmware_id)->toBe($firmware->id);
});

View file

@ -0,0 +1,154 @@
<?php
declare(strict_types=1);
use App\Models\Device;
use App\Models\Playlist;
use App\Models\PlaylistItem;
use App\Models\Plugin;
use App\Models\User;
test('mashup create command has correct signature', function () {
$this->artisan('mashup:create --help')
->assertExitCode(0);
});
test('mashup create command creates mashup successfully', function () {
$user = User::factory()->create();
$device = Device::factory()->create(['user_id' => $user->id]);
$playlist = Playlist::factory()->create(['device_id' => $device->id]);
$plugin1 = Plugin::factory()->create(['user_id' => $user->id]);
$plugin2 = Plugin::factory()->create(['user_id' => $user->id]);
$this->artisan('mashup:create')
->expectsQuestion('Select a device', $device->id)
->expectsQuestion('Select a playlist', $playlist->id)
->expectsQuestion('Select a layout', '1Lx1R')
->expectsQuestion('Enter a name for this mashup', 'Test Mashup')
->expectsQuestion('Select the first plugin', $plugin1->id)
->expectsQuestion('Select the second plugin', $plugin2->id)
->expectsOutput('Mashup created successfully!')
->assertExitCode(0);
$playlistItem = PlaylistItem::where('playlist_id', $playlist->id)
->whereJsonContains('mashup->mashup_name', 'Test Mashup')
->first();
expect($playlistItem)->not->toBeNull();
expect($playlistItem->isMashup())->toBeTrue();
expect($playlistItem->getMashupLayoutType())->toBe('1Lx1R');
expect($playlistItem->getMashupPluginIds())->toContain($plugin1->id, $plugin2->id);
});
test('mashup create command exits when no devices found', function () {
$this->artisan('mashup:create')
->expectsOutput('No devices found. Please create a device first.')
->assertExitCode(1);
});
test('mashup create command exits when no playlists found for device', function () {
$user = User::factory()->create();
$device = Device::factory()->create(['user_id' => $user->id]);
$this->artisan('mashup:create')
->expectsQuestion('Select a device', $device->id)
->expectsOutput('No playlists found for this device. Please create a playlist first.')
->assertExitCode(1);
});
test('mashup create command exits when no plugins found', function () {
$user = User::factory()->create();
$device = Device::factory()->create(['user_id' => $user->id]);
$playlist = Playlist::factory()->create(['device_id' => $device->id]);
$this->artisan('mashup:create')
->expectsQuestion('Select a device', $device->id)
->expectsQuestion('Select a playlist', $playlist->id)
->expectsQuestion('Select a layout', '1Lx1R')
->expectsQuestion('Enter a name for this mashup', 'Test Mashup')
->expectsOutput('No plugins found. Please create some plugins first.')
->assertExitCode(1);
});
test('mashup create command validates mashup name length', function () {
$user = User::factory()->create();
$device = Device::factory()->create(['user_id' => $user->id]);
$playlist = Playlist::factory()->create(['device_id' => $device->id]);
$plugin1 = Plugin::factory()->create(['user_id' => $user->id]);
$plugin2 = Plugin::factory()->create(['user_id' => $user->id]);
$this->artisan('mashup:create')
->expectsQuestion('Select a device', $device->id)
->expectsQuestion('Select a playlist', $playlist->id)
->expectsQuestion('Select a layout', '1Lx1R')
->expectsQuestion('Enter a name for this mashup', 'A') // Too short
->expectsOutput('The name must be at least 2 characters.')
->assertExitCode(1);
});
test('mashup create command validates mashup name maximum length', function () {
$user = User::factory()->create();
$device = Device::factory()->create(['user_id' => $user->id]);
$playlist = Playlist::factory()->create(['device_id' => $device->id]);
$plugin1 = Plugin::factory()->create(['user_id' => $user->id]);
$plugin2 = Plugin::factory()->create(['user_id' => $user->id]);
$longName = str_repeat('A', 51); // Too long
$this->artisan('mashup:create')
->expectsQuestion('Select a device', $device->id)
->expectsQuestion('Select a playlist', $playlist->id)
->expectsQuestion('Select a layout', '1Lx1R')
->expectsQuestion('Enter a name for this mashup', $longName)
->expectsOutput('The name must not exceed 50 characters.')
->assertExitCode(1);
});
test('mashup create command uses default name when provided', function () {
$user = User::factory()->create();
$device = Device::factory()->create(['user_id' => $user->id]);
$playlist = Playlist::factory()->create(['device_id' => $device->id]);
$plugin1 = Plugin::factory()->create(['user_id' => $user->id]);
$plugin2 = Plugin::factory()->create(['user_id' => $user->id]);
$this->artisan('mashup:create')
->expectsQuestion('Select a device', $device->id)
->expectsQuestion('Select a playlist', $playlist->id)
->expectsQuestion('Select a layout', '1Lx1R')
->expectsQuestion('Enter a name for this mashup', 'Mashup') // Default value
->expectsQuestion('Select the first plugin', $plugin1->id)
->expectsQuestion('Select the second plugin', $plugin2->id)
->expectsOutput('Mashup created successfully!')
->assertExitCode(0);
$playlistItem = PlaylistItem::where('playlist_id', $playlist->id)
->whereJsonContains('mashup->mashup_name', 'Mashup')
->first();
expect($playlistItem)->not->toBeNull();
});
test('mashup create command handles 1x1 layout with single plugin', function () {
$user = User::factory()->create();
$device = Device::factory()->create(['user_id' => $user->id]);
$playlist = Playlist::factory()->create(['device_id' => $device->id]);
$plugin = Plugin::factory()->create(['user_id' => $user->id]);
$this->artisan('mashup:create')
->expectsQuestion('Select a device', $device->id)
->expectsQuestion('Select a playlist', $playlist->id)
->expectsQuestion('Select a layout', '1x1')
->expectsQuestion('Enter a name for this mashup', 'Single Plugin Mashup')
->expectsQuestion('Select the first plugin', $plugin->id)
->expectsOutput('Mashup created successfully!')
->assertExitCode(0);
$playlistItem = PlaylistItem::where('playlist_id', $playlist->id)
->whereJsonContains('mashup->mashup_name', 'Single Plugin Mashup')
->first();
expect($playlistItem)->not->toBeNull();
expect($playlistItem->getMashupLayoutType())->toBe('1x1');
expect($playlistItem->getMashupPluginIds())->toHaveCount(1);
expect($playlistItem->getMashupPluginIds())->toContain($plugin->id);
});

View file

@ -0,0 +1,188 @@
<?php
declare(strict_types=1);
use function Pest\Laravel\mock;
test('oidc test command has correct signature', function () {
$this->artisan('oidc:test --help')
->assertExitCode(0);
});
test('oidc test command runs successfully with disabled oidc', function () {
config(['services.oidc.enabled' => false]);
$this->artisan('oidc:test')
->expectsOutput('Testing OIDC Configuration...')
->expectsOutput('OIDC Enabled: ❌ No')
->expectsOutput('OIDC Endpoint: ❌ Not set')
->expectsOutput('Client ID: ❌ Not set')
->expectsOutput('Client Secret: ❌ Not set')
->expectsOutput('Redirect URL: ✅ http://localhost/auth/oidc/callback')
->expectsOutput('Scopes: ✅ openid, profile, email')
->expectsOutput('OIDC Driver: ✅ Registered (configuration test skipped due to missing values)')
->expectsOutput('⚠️ OIDC driver is registered but missing required configuration.')
->expectsOutput('Please set the following environment variables:')
->expectsOutput(' - OIDC_ENABLED=true')
->expectsOutput(' - OIDC_ENDPOINT=https://your-oidc-provider.com (base URL)')
->expectsOutput(' OR')
->expectsOutput(' - OIDC_ENDPOINT=https://your-oidc-provider.com/.well-known/openid-configuration (full URL)')
->expectsOutput(' - OIDC_CLIENT_ID=your-client-id')
->expectsOutput(' - OIDC_CLIENT_SECRET=your-client-secret')
->assertExitCode(0);
});
test('oidc test command runs successfully with enabled oidc but missing config', function () {
config([
'services.oidc.enabled' => true,
'services.oidc.endpoint' => null,
'services.oidc.client_id' => null,
'services.oidc.client_secret' => null,
'services.oidc.redirect' => null,
'services.oidc.scopes' => [],
]);
$this->artisan('oidc:test')
->expectsOutput('Testing OIDC Configuration...')
->expectsOutput('OIDC Enabled: ✅ Yes')
->expectsOutput('OIDC Endpoint: ❌ Not set')
->expectsOutput('Client ID: ❌ Not set')
->expectsOutput('Client Secret: ❌ Not set')
->expectsOutput('Redirect URL: ✅ http://localhost/auth/oidc/callback')
->expectsOutput('Scopes: ✅ openid, profile, email')
->expectsOutput('OIDC Driver: ✅ Registered (configuration test skipped due to missing values)')
->expectsOutput('⚠️ OIDC driver is registered but missing required configuration.')
->expectsOutput('Please set the following environment variables:')
->expectsOutput(' - OIDC_ENDPOINT=https://your-oidc-provider.com (base URL)')
->expectsOutput(' OR')
->expectsOutput(' - OIDC_ENDPOINT=https://your-oidc-provider.com/.well-known/openid-configuration (full URL)')
->expectsOutput(' - OIDC_CLIENT_ID=your-client-id')
->expectsOutput(' - OIDC_CLIENT_SECRET=your-client-secret')
->assertExitCode(0);
});
test('oidc test command runs successfully with partial config', function () {
config([
'services.oidc.enabled' => true,
'services.oidc.endpoint' => 'https://example.com',
'services.oidc.client_id' => 'test-client-id',
'services.oidc.client_secret' => null,
'services.oidc.redirect' => 'https://example.com/callback',
'services.oidc.scopes' => ['openid', 'profile'],
]);
$this->artisan('oidc:test')
->expectsOutput('Testing OIDC Configuration...')
->expectsOutput('OIDC Enabled: ✅ Yes')
->expectsOutput('OIDC Endpoint: ✅ https://example.com')
->expectsOutput('Client ID: ✅ test-client-id')
->expectsOutput('Client Secret: ❌ Not set')
->expectsOutput('Redirect URL: ✅ https://example.com/callback')
->expectsOutput('Scopes: ✅ openid, profile')
->expectsOutput('OIDC Driver: ✅ Registered (configuration test skipped due to missing values)')
->expectsOutput('⚠️ OIDC driver is registered but missing required configuration.')
->expectsOutput('Please set the following environment variables:')
->expectsOutput(' - OIDC_CLIENT_SECRET=your-client-secret')
->assertExitCode(0);
});
test('oidc test command runs successfully with full config but disabled', function () {
// Mock the HTTP client to return fake OIDC configuration
mock(GuzzleHttp\Client::class, function ($mock) {
$mock->shouldReceive('get')
->with('https://example.com/.well-known/openid-configuration')
->andReturn(new GuzzleHttp\Psr7\Response(200, [], json_encode([
'authorization_endpoint' => 'https://example.com/auth',
'token_endpoint' => 'https://example.com/token',
'userinfo_endpoint' => 'https://example.com/userinfo',
])));
});
config([
'services.oidc.enabled' => false,
'services.oidc.endpoint' => 'https://example.com',
'services.oidc.client_id' => 'test-client-id',
'services.oidc.client_secret' => 'test-client-secret',
'services.oidc.redirect' => 'https://example.com/callback',
'services.oidc.scopes' => ['openid', 'profile'],
]);
$this->artisan('oidc:test')
->expectsOutput('Testing OIDC Configuration...')
->expectsOutput('OIDC Enabled: ❌ No')
->expectsOutput('OIDC Endpoint: ✅ https://example.com')
->expectsOutput('Client ID: ✅ test-client-id')
->expectsOutput('Client Secret: ✅ Set')
->expectsOutput('Redirect URL: ✅ https://example.com/callback')
->expectsOutput('Scopes: ✅ openid, profile')
->expectsOutput('OIDC Driver: ✅ Successfully registered and accessible')
->expectsOutput('⚠️ OIDC driver is working but OIDC_ENABLED is false.')
->assertExitCode(0);
});
test('oidc test command runs successfully with full config and enabled', function () {
// Mock the HTTP client to return fake OIDC configuration
mock(GuzzleHttp\Client::class, function ($mock) {
$mock->shouldReceive('get')
->with('https://example.com/.well-known/openid-configuration')
->andReturn(new GuzzleHttp\Psr7\Response(200, [], json_encode([
'authorization_endpoint' => 'https://example.com/auth',
'token_endpoint' => 'https://example.com/token',
'userinfo_endpoint' => 'https://example.com/userinfo',
])));
});
config([
'services.oidc.enabled' => true,
'services.oidc.endpoint' => 'https://example.com',
'services.oidc.client_id' => 'test-client-id',
'services.oidc.client_secret' => 'test-client-secret',
'services.oidc.redirect' => 'https://example.com/callback',
'services.oidc.scopes' => ['openid', 'profile'],
]);
$this->artisan('oidc:test')
->expectsOutput('Testing OIDC Configuration...')
->expectsOutput('OIDC Enabled: ✅ Yes')
->expectsOutput('OIDC Endpoint: ✅ https://example.com')
->expectsOutput('Client ID: ✅ test-client-id')
->expectsOutput('Client Secret: ✅ Set')
->expectsOutput('Redirect URL: ✅ https://example.com/callback')
->expectsOutput('Scopes: ✅ openid, profile')
->expectsOutput('OIDC Driver: ✅ Successfully registered and accessible')
->expectsOutput('✅ OIDC is fully configured and ready to use!')
->expectsOutput('You can test the login flow at: /auth/oidc/redirect')
->assertExitCode(0);
});
test('oidc test command handles empty scopes', function () {
// Mock the HTTP client to return fake OIDC configuration
mock(GuzzleHttp\Client::class, function ($mock) {
$mock->shouldReceive('get')
->with('https://example.com/.well-known/openid-configuration')
->andReturn(new GuzzleHttp\Psr7\Response(200, [], json_encode([
'authorization_endpoint' => 'https://example.com/auth',
'token_endpoint' => 'https://example.com/token',
'userinfo_endpoint' => 'https://example.com/userinfo',
])));
});
config([
'services.oidc.enabled' => false,
'services.oidc.endpoint' => 'https://example.com',
'services.oidc.client_id' => 'test-client-id',
'services.oidc.client_secret' => 'test-client-secret',
'services.oidc.redirect' => 'https://example.com/callback',
'services.oidc.scopes' => null,
]);
$this->artisan('oidc:test')
->expectsOutput('Testing OIDC Configuration...')
->expectsOutput('OIDC Enabled: ❌ No')
->expectsOutput('OIDC Endpoint: ✅ https://example.com')
->expectsOutput('Client ID: ✅ test-client-id')
->expectsOutput('Client Secret: ✅ Set')
->expectsOutput('Redirect URL: ✅ https://example.com/callback')
->expectsOutput('Scopes: ✅ openid, profile, email')
->assertExitCode(0);
});

View file

@ -0,0 +1,344 @@
<?php
declare(strict_types=1);
use App\Jobs\FetchDeviceModelsJob;
use App\Models\DeviceModel;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Facades\Log;
uses(RefreshDatabase::class);
beforeEach(function (): void {
DeviceModel::truncate();
});
test('fetch device models job can be dispatched', function () {
$job = new FetchDeviceModelsJob();
expect($job)->toBeInstanceOf(FetchDeviceModelsJob::class);
});
test('fetch device models job handles successful api response', function () {
Http::fake([
'usetrmnl.com/api/models' => Http::response([
'data' => [
[
'name' => 'test-model',
'label' => 'Test Model',
'description' => 'A test device model',
'width' => 800,
'height' => 480,
'colors' => 4,
'bit_depth' => 2,
'scale_factor' => 1.0,
'rotation' => 0,
'mime_type' => 'image/png',
'offset_x' => 0,
'offset_y' => 0,
'published_at' => '2023-01-01T00:00:00Z',
],
],
], 200),
]);
Log::shouldReceive('info')
->once()
->with('Successfully fetched and updated device models', ['count' => 1]);
$job = new FetchDeviceModelsJob();
$job->handle();
$deviceModel = DeviceModel::where('name', 'test-model')->first();
expect($deviceModel)->not->toBeNull();
expect($deviceModel->label)->toBe('Test Model');
expect($deviceModel->description)->toBe('A test device 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->mime_type)->toBe('image/png');
expect($deviceModel->offset_x)->toBe(0);
expect($deviceModel->offset_y)->toBe(0);
expect($deviceModel->source)->toBe('api');
});
test('fetch device models job handles multiple device models', function () {
Http::fake([
'usetrmnl.com/api/models' => Http::response([
'data' => [
[
'name' => 'model-1',
'label' => 'Model 1',
'description' => 'First model',
'width' => 800,
'height' => 480,
'colors' => 4,
'bit_depth' => 2,
'scale_factor' => 1.0,
'rotation' => 0,
'mime_type' => 'image/png',
'offset_x' => 0,
'offset_y' => 0,
'published_at' => '2023-01-01T00:00:00Z',
],
[
'name' => 'model-2',
'label' => 'Model 2',
'description' => 'Second model',
'width' => 1200,
'height' => 800,
'colors' => 16,
'bit_depth' => 4,
'scale_factor' => 1.5,
'rotation' => 90,
'mime_type' => 'image/bmp',
'offset_x' => 10,
'offset_y' => 20,
'published_at' => '2023-01-02T00:00:00Z',
],
],
], 200),
]);
Log::shouldReceive('info')
->once()
->with('Successfully fetched and updated device models', ['count' => 2]);
$job = new FetchDeviceModelsJob();
$job->handle();
expect(DeviceModel::where('name', 'model-1')->exists())->toBeTrue();
expect(DeviceModel::where('name', 'model-2')->exists())->toBeTrue();
});
test('fetch device models job handles empty data array', function () {
Http::fake([
'usetrmnl.com/api/models' => Http::response([
'data' => [],
], 200),
]);
Log::shouldReceive('info')
->once()
->with('Successfully fetched and updated device models', ['count' => 0]);
$job = new FetchDeviceModelsJob();
$job->handle();
expect(DeviceModel::count())->toBe(0);
});
test('fetch device models job handles missing data field', function () {
Http::fake([
'usetrmnl.com/api/models' => Http::response([
'message' => 'No data available',
], 200),
]);
Log::shouldReceive('info')
->once()
->with('Successfully fetched and updated device models', ['count' => 0]);
$job = new FetchDeviceModelsJob();
$job->handle();
expect(DeviceModel::count())->toBe(0);
});
test('fetch device models job handles non-array data', function () {
Http::fake([
'usetrmnl.com/api/models' => Http::response([
'data' => 'invalid-data',
], 200),
]);
Log::shouldReceive('error')
->once()
->with('Invalid response format from device models API', Mockery::type('array'));
$job = new FetchDeviceModelsJob();
$job->handle();
expect(DeviceModel::count())->toBe(0);
});
test('fetch device models job handles api failure', function () {
Http::fake([
'usetrmnl.com/api/models' => Http::response([
'error' => 'Internal Server Error',
], 500),
]);
Log::shouldReceive('error')
->once()
->with('Failed to fetch device models from API', [
'status' => 500,
'body' => '{"error":"Internal Server Error"}',
]);
$job = new FetchDeviceModelsJob();
$job->handle();
expect(DeviceModel::count())->toBe(0);
});
test('fetch device models job handles network exception', function () {
Http::fake([
'usetrmnl.com/api/models' => function () {
throw new Exception('Network connection failed');
},
]);
Log::shouldReceive('error')
->once()
->with('Exception occurred while fetching device models', Mockery::type('array'));
$job = new FetchDeviceModelsJob();
$job->handle();
expect(DeviceModel::count())->toBe(0);
});
test('fetch device models job handles device model with missing name', function () {
Http::fake([
'usetrmnl.com/api/models' => Http::response([
'data' => [
[
'label' => 'Model without name',
'description' => 'This model has no name',
],
],
], 200),
]);
Log::shouldReceive('warning')
->once()
->with('Device model data missing name field', Mockery::type('array'));
Log::shouldReceive('info')
->once()
->with('Successfully fetched and updated device models', ['count' => 1]);
$job = new FetchDeviceModelsJob();
$job->handle();
expect(DeviceModel::count())->toBe(0);
});
test('fetch device models job handles device model with partial data', function () {
Http::fake([
'usetrmnl.com/api/models' => Http::response([
'data' => [
[
'name' => 'minimal-model',
// Only name provided, other fields should use defaults
],
],
], 200),
]);
Log::shouldReceive('info')
->once()
->with('Successfully fetched and updated device models', ['count' => 1]);
$job = new FetchDeviceModelsJob();
$job->handle();
$deviceModel = DeviceModel::where('name', 'minimal-model')->first();
expect($deviceModel)->not->toBeNull();
expect($deviceModel->label)->toBe('');
expect($deviceModel->description)->toBe('');
expect($deviceModel->width)->toBe(0);
expect($deviceModel->height)->toBe(0);
expect($deviceModel->colors)->toBe(0);
expect($deviceModel->bit_depth)->toBe(0);
expect($deviceModel->scale_factor)->toBe(1.0);
expect($deviceModel->rotation)->toBe(0);
expect($deviceModel->mime_type)->toBe('');
expect($deviceModel->offset_x)->toBe(0);
expect($deviceModel->offset_y)->toBe(0);
expect($deviceModel->source)->toBe('api');
});
test('fetch device models job updates existing device model', function () {
// Create an existing device model
$existingModel = DeviceModel::factory()->create([
'name' => 'existing-model',
'label' => 'Old Label',
'width' => 400,
'height' => 300,
]);
Http::fake([
'usetrmnl.com/api/models' => Http::response([
'data' => [
[
'name' => 'existing-model',
'label' => 'Updated Label',
'description' => 'Updated description',
'width' => 800,
'height' => 600,
'colors' => 4,
'bit_depth' => 2,
'scale_factor' => 1.0,
'rotation' => 0,
'mime_type' => 'image/png',
'offset_x' => 0,
'offset_y' => 0,
'published_at' => '2023-01-01T00:00:00Z',
],
],
], 200),
]);
Log::shouldReceive('info')
->once()
->with('Successfully fetched and updated device models', ['count' => 1]);
$job = new FetchDeviceModelsJob();
$job->handle();
$existingModel->refresh();
expect($existingModel->label)->toBe('Updated Label');
expect($existingModel->description)->toBe('Updated description');
expect($existingModel->width)->toBe(800);
expect($existingModel->height)->toBe(600);
expect($existingModel->source)->toBe('api');
});
test('fetch device models job handles processing exception for individual model', function () {
Http::fake([
'usetrmnl.com/api/models' => Http::response([
'data' => [
[
'name' => 'valid-model',
'label' => 'Valid Model',
'width' => 800,
'height' => 480,
],
[
'name' => null, // This will cause an exception in processing
'label' => 'Invalid Model',
],
],
], 200),
]);
Log::shouldReceive('warning')
->once()
->with('Device model data missing name field', Mockery::type('array'));
Log::shouldReceive('info')
->once()
->with('Successfully fetched and updated device models', ['count' => 2]);
$job = new FetchDeviceModelsJob();
$job->handle();
// Should still create the valid model
expect(DeviceModel::where('name', 'valid-model')->exists())->toBeTrue();
expect(DeviceModel::count())->toBe(1);
});

View file

@ -14,6 +14,7 @@ test('it creates firmwares directory if it does not exist', function () {
$firmware = Firmware::factory()->create([
'url' => 'https://example.com/firmware.bin',
'version_tag' => '1.0.0',
'storage_location' => null,
]);
Http::fake([
@ -33,9 +34,127 @@ test('it downloads firmware and updates storage location', function () {
$firmware = Firmware::factory()->create([
'url' => 'https://example.com/firmware.bin',
'version_tag' => '1.0.0',
'storage_location' => null,
]);
(new FirmwareDownloadJob($firmware))->handle();
expect($firmware->fresh()->storage_location)->toBe('firmwares/FW1.0.0.bin');
});
test('it handles connection exception gracefully', function () {
$firmware = Firmware::factory()->create([
'url' => 'https://example.com/firmware.bin',
'version_tag' => '1.0.0',
'storage_location' => null,
]);
Http::fake([
'https://example.com/firmware.bin' => function () {
throw new Illuminate\Http\Client\ConnectionException('Connection failed');
},
]);
Illuminate\Support\Facades\Log::shouldReceive('error')
->once()
->with('Firmware download failed: Connection failed');
(new FirmwareDownloadJob($firmware))->handle();
// Storage location should not be updated on failure
expect($firmware->fresh()->storage_location)->toBeNull();
});
test('it handles general exception gracefully', function () {
$firmware = Firmware::factory()->create([
'url' => 'https://example.com/firmware.bin',
'version_tag' => '1.0.0',
'storage_location' => null,
]);
Http::fake([
'https://example.com/firmware.bin' => function () {
throw new Exception('Unexpected error');
},
]);
Illuminate\Support\Facades\Log::shouldReceive('error')
->once()
->with('An unexpected error occurred: Unexpected error');
(new FirmwareDownloadJob($firmware))->handle();
// Storage location should not be updated on failure
expect($firmware->fresh()->storage_location)->toBeNull();
});
test('it handles firmware with special characters in version tag', function () {
Http::fake([
'https://example.com/firmware.bin' => Http::response('fake firmware content', 200),
]);
$firmware = Firmware::factory()->create([
'url' => 'https://example.com/firmware.bin',
'version_tag' => '1.0.0-beta',
]);
(new FirmwareDownloadJob($firmware))->handle();
expect($firmware->fresh()->storage_location)->toBe('firmwares/FW1.0.0-beta.bin');
});
test('it handles firmware with long version tag', function () {
Http::fake([
'https://example.com/firmware.bin' => Http::response('fake firmware content', 200),
]);
$firmware = Firmware::factory()->create([
'url' => 'https://example.com/firmware.bin',
'version_tag' => '1.0.0.1234.5678.90',
]);
(new FirmwareDownloadJob($firmware))->handle();
expect($firmware->fresh()->storage_location)->toBe('firmwares/FW1.0.0.1234.5678.90.bin');
});
test('it creates firmwares directory even when it already exists', function () {
$firmware = Firmware::factory()->create([
'url' => 'https://example.com/firmware.bin',
'version_tag' => '1.0.0',
'storage_location' => null,
]);
Http::fake([
'https://example.com/firmware.bin' => Http::response('fake firmware content', 200),
]);
// Directory already exists from beforeEach
expect(Storage::disk('public')->exists('firmwares'))->toBeTrue();
(new FirmwareDownloadJob($firmware))->handle();
// Should still work fine
expect($firmware->fresh()->storage_location)->toBe('firmwares/FW1.0.0.bin');
});
test('it handles http error response', function () {
$firmware = Firmware::factory()->create([
'url' => 'https://example.com/firmware.bin',
'version_tag' => '1.0.0',
'storage_location' => null,
]);
Http::fake([
'https://example.com/firmware.bin' => Http::response('Not Found', 404),
]);
Illuminate\Support\Facades\Log::shouldReceive('error')
->once()
->with(Mockery::type('string'));
(new FirmwareDownloadJob($firmware))->handle();
// Storage location should not be updated on failure
expect($firmware->fresh()->storage_location)->toBeNull();
});

View file

@ -10,9 +10,9 @@ 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,
@ -24,16 +24,16 @@ test('it sends battery low notification when battery is below threshold', functi
$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,
@ -45,16 +45,16 @@ test('it does not send notification when battery is above threshold', function (
$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,
@ -70,9 +70,9 @@ test('it does not send notification when already sent', function () {
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,
@ -84,16 +84,16 @@ test('it resets notification flag when battery is above threshold', function ()
$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
@ -108,18 +108,18 @@ test('it skips devices without associated user', function () {
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
@ -131,10 +131,10 @@ test('it processes multiple devices correctly', function () {
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,115 @@
<?php
declare(strict_types=1);
use App\Livewire\Actions\DeviceAutoJoin;
use App\Models\User;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Livewire\Livewire;
uses(RefreshDatabase::class);
test('device auto join component can be rendered', function () {
$user = User::factory()->create(['assign_new_devices' => false]);
Livewire::actingAs($user)
->test(DeviceAutoJoin::class)
->assertSee('Permit Auto-Join')
->assertSet('deviceAutojoin', false)
->assertSet('isFirstUser', true);
});
test('device auto join component initializes with user settings', function () {
$user = User::factory()->create(['assign_new_devices' => true]);
Livewire::actingAs($user)
->test(DeviceAutoJoin::class)
->assertSet('deviceAutojoin', true)
->assertSet('isFirstUser', true);
});
test('device auto join component identifies first user correctly', function () {
$firstUser = User::factory()->create(['id' => 1, 'assign_new_devices' => false]);
$otherUser = User::factory()->create(['id' => 2, 'assign_new_devices' => false]);
Livewire::actingAs($firstUser)
->test(DeviceAutoJoin::class)
->assertSet('isFirstUser', true);
Livewire::actingAs($otherUser)
->test(DeviceAutoJoin::class)
->assertSet('isFirstUser', false);
});
test('device auto join component updates user setting when toggled', function () {
$user = User::factory()->create(['assign_new_devices' => false]);
Livewire::actingAs($user)
->test(DeviceAutoJoin::class)
->set('deviceAutojoin', true)
->assertSet('deviceAutojoin', true);
$user->refresh();
expect($user->assign_new_devices)->toBeTrue();
});
// Validation test removed - Livewire automatically handles boolean conversion
test('device auto join component handles false value correctly', function () {
$user = User::factory()->create(['assign_new_devices' => true]);
Livewire::actingAs($user)
->test(DeviceAutoJoin::class)
->set('deviceAutojoin', false)
->assertSet('deviceAutojoin', false);
$user->refresh();
expect($user->assign_new_devices)->toBeFalse();
});
test('device auto join component only updates when deviceAutojoin property changes', function () {
$user = User::factory()->create(['assign_new_devices' => false]);
$component = Livewire::actingAs($user)
->test(DeviceAutoJoin::class);
// Set a different property to ensure it doesn't trigger the update
$component->set('isFirstUser', true);
$user->refresh();
expect($user->assign_new_devices)->toBeFalse();
});
test('device auto join component renders correct view', function () {
$user = User::factory()->create();
Livewire::actingAs($user)
->test(DeviceAutoJoin::class)
->assertViewIs('livewire.actions.device-auto-join');
});
test('device auto join component works with authenticated user', function () {
$user = User::factory()->create(['assign_new_devices' => true]);
$component = Livewire::actingAs($user)
->test(DeviceAutoJoin::class);
expect($component->instance()->deviceAutojoin)->toBeTrue();
expect($component->instance()->isFirstUser)->toBe($user->id === 1);
});
test('device auto join component handles multiple updates correctly', function () {
$user = User::factory()->create(['assign_new_devices' => false]);
$component = Livewire::actingAs($user)
->test(DeviceAutoJoin::class)
->set('deviceAutojoin', true);
$user->refresh();
expect($user->assign_new_devices)->toBeTrue();
$component->set('deviceAutojoin', false);
$user->refresh();
expect($user->assign_new_devices)->toBeFalse();
});