mirror of
https://github.com/usetrmnl/byos_laravel.git
synced 2026-01-14 07:27:47 +00:00
This commit is contained in:
parent
4f251bf37e
commit
42b515e322
21 changed files with 2212 additions and 32 deletions
344
tests/Feature/Jobs/FetchDeviceModelsJobTest.php
Normal file
344
tests/Feature/Jobs/FetchDeviceModelsJobTest.php
Normal 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);
|
||||
});
|
||||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
140
tests/Feature/Jobs/NotifyDeviceBatteryLowJobTest.php
Normal file
140
tests/Feature/Jobs/NotifyDeviceBatteryLowJobTest.php
Normal 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();
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue