mirror of
https://github.com/usetrmnl/byos_laravel.git
synced 2026-01-14 15:37:53 +00:00
This commit is contained in:
parent
e9fc6b2335
commit
5d3a512203
20 changed files with 1651 additions and 101 deletions
|
|
@ -9,9 +9,6 @@ use App\Models\Plugin;
|
||||||
use Illuminate\Console\Command;
|
use Illuminate\Console\Command;
|
||||||
use Illuminate\Support\Collection;
|
use Illuminate\Support\Collection;
|
||||||
|
|
||||||
use function Laravel\Prompts\select;
|
|
||||||
use function Laravel\Prompts\text;
|
|
||||||
|
|
||||||
class MashupCreateCommand extends Command
|
class MashupCreateCommand extends Command
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
|
|
@ -88,9 +85,9 @@ class MashupCreateCommand extends Command
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$deviceId = select(
|
$deviceId = $this->choice(
|
||||||
label: 'Select a device',
|
'Select a device',
|
||||||
options: $devices->mapWithKeys(fn ($device) => [$device->id => $device->name])->toArray()
|
$devices->mapWithKeys(fn ($device) => [$device->id => $device->name])->toArray()
|
||||||
);
|
);
|
||||||
|
|
||||||
return $devices->firstWhere('id', $deviceId);
|
return $devices->firstWhere('id', $deviceId);
|
||||||
|
|
@ -106,9 +103,9 @@ class MashupCreateCommand extends Command
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
$playlistId = select(
|
$playlistId = $this->choice(
|
||||||
label: 'Select a playlist',
|
'Select a playlist',
|
||||||
options: $playlists->mapWithKeys(fn (Playlist $playlist) => [$playlist->id => $playlist->name])->toArray()
|
$playlists->mapWithKeys(fn (Playlist $playlist) => [$playlist->id => $playlist->name])->toArray()
|
||||||
);
|
);
|
||||||
|
|
||||||
return $playlists->firstWhere('id', $playlistId);
|
return $playlists->firstWhere('id', $playlistId);
|
||||||
|
|
@ -116,24 +113,29 @@ class MashupCreateCommand extends Command
|
||||||
|
|
||||||
protected function selectLayout(): ?string
|
protected function selectLayout(): ?string
|
||||||
{
|
{
|
||||||
return select(
|
return $this->choice(
|
||||||
label: 'Select a layout',
|
'Select a layout',
|
||||||
options: PlaylistItem::getAvailableLayouts()
|
PlaylistItem::getAvailableLayouts()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getMashupName(): ?string
|
protected function getMashupName(): ?string
|
||||||
{
|
{
|
||||||
return text(
|
$name = $this->ask('Enter a name for this mashup', 'Mashup');
|
||||||
label: 'Enter a name for this mashup',
|
|
||||||
required: true,
|
if (mb_strlen($name) < 2) {
|
||||||
default: 'Mashup',
|
$this->error('The name must be at least 2 characters.');
|
||||||
validate: fn (string $value) => match (true) {
|
|
||||||
mb_strlen($value) < 1 => 'The name must be at least 2 characters.',
|
return null;
|
||||||
mb_strlen($value) > 50 => 'The name must not exceed 50 characters.',
|
|
||||||
default => null,
|
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
if (mb_strlen($name) > 50) {
|
||||||
|
$this->error('The name must not exceed 50 characters.');
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $name;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function selectPlugins(string $layout): Collection
|
protected function selectPlugins(string $layout): Collection
|
||||||
|
|
@ -159,9 +161,9 @@ class MashupCreateCommand extends Command
|
||||||
default => ($i + 1).'th'
|
default => ($i + 1).'th'
|
||||||
};
|
};
|
||||||
|
|
||||||
$pluginId = select(
|
$pluginId = $this->choice(
|
||||||
label: "Select the $position plugin",
|
"Select the $position plugin",
|
||||||
options: $availablePlugins
|
$availablePlugins
|
||||||
);
|
);
|
||||||
|
|
||||||
$selectedPlugins->push($plugins->firstWhere('id', $pluginId));
|
$selectedPlugins->push($plugins->firstWhere('id', $pluginId));
|
||||||
|
|
|
||||||
|
|
@ -40,13 +40,18 @@ class OidcTestCommand extends Command
|
||||||
$clientId = config('services.oidc.client_id');
|
$clientId = config('services.oidc.client_id');
|
||||||
$clientSecret = config('services.oidc.client_secret');
|
$clientSecret = config('services.oidc.client_secret');
|
||||||
$redirect = config('services.oidc.redirect');
|
$redirect = config('services.oidc.redirect');
|
||||||
|
if (! $redirect) {
|
||||||
|
$redirect = config('app.url', 'http://localhost').'/auth/oidc/callback';
|
||||||
|
}
|
||||||
$scopes = config('services.oidc.scopes', []);
|
$scopes = config('services.oidc.scopes', []);
|
||||||
|
$defaultScopes = ['openid', 'profile', 'email'];
|
||||||
|
$effectiveScopes = empty($scopes) ? $defaultScopes : $scopes;
|
||||||
|
|
||||||
$this->line('OIDC Endpoint: '.($endpoint ? "✅ {$endpoint}" : '❌ Not set'));
|
$this->line('OIDC Endpoint: '.($endpoint ? "✅ {$endpoint}" : '❌ Not set'));
|
||||||
$this->line('Client ID: '.($clientId ? "✅ {$clientId}" : '❌ Not set'));
|
$this->line('Client ID: '.($clientId ? "✅ {$clientId}" : '❌ Not set'));
|
||||||
$this->line('Client Secret: '.($clientSecret ? '✅ Set' : '❌ Not set'));
|
$this->line('Client Secret: '.($clientSecret ? '✅ Set' : '❌ Not set'));
|
||||||
$this->line('Redirect URL: '.($redirect ? "✅ {$redirect}" : '❌ Not set'));
|
$this->line('Redirect URL: '.($redirect ? "✅ {$redirect}" : '❌ Not set'));
|
||||||
$this->line('Scopes: '.(empty($scopes) ? '❌ Not set' : '✅ '.implode(', ', $scopes)));
|
$this->line('Scopes: ✅ '.implode(', ', $effectiveScopes));
|
||||||
|
|
||||||
$this->newLine();
|
$this->newLine();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -33,16 +33,25 @@ class FirmwareDownloadJob implements ShouldQueue
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$filename = "FW{$this->firmware->version_tag}.bin";
|
$filename = "FW{$this->firmware->version_tag}.bin";
|
||||||
Http::sink(storage_path("app/public/firmwares/$filename"))
|
$response = Http::get($this->firmware->url);
|
||||||
->get($this->firmware->url);
|
|
||||||
|
|
||||||
|
if (! $response->successful()) {
|
||||||
|
throw new Exception('HTTP request failed with status: '.$response->status());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the response content to file
|
||||||
|
Storage::disk('public')->put("firmwares/$filename", $response->body());
|
||||||
|
|
||||||
|
// Only update storage location if download was successful
|
||||||
$this->firmware->update([
|
$this->firmware->update([
|
||||||
'storage_location' => "firmwares/$filename",
|
'storage_location' => "firmwares/$filename",
|
||||||
]);
|
]);
|
||||||
} catch (ConnectionException $e) {
|
} catch (ConnectionException $e) {
|
||||||
Log::error('Firmware download failed: '.$e->getMessage());
|
Log::error('Firmware download failed: '.$e->getMessage());
|
||||||
|
// Don't update storage_location on failure
|
||||||
} catch (Exception $e) {
|
} catch (Exception $e) {
|
||||||
Log::error('An unexpected error occurred: '.$e->getMessage());
|
Log::error('An unexpected error occurred: '.$e->getMessage());
|
||||||
|
// Don't update storage_location on failure
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -60,7 +60,7 @@ class OidcProvider extends AbstractProvider implements ProviderInterface
|
||||||
{
|
{
|
||||||
try {
|
try {
|
||||||
$url = $this->baseUrl.'/.well-known/openid-configuration';
|
$url = $this->baseUrl.'/.well-known/openid-configuration';
|
||||||
$client = new Client();
|
$client = app(Client::class);
|
||||||
$response = $client->get($url);
|
$response = $client->get($url);
|
||||||
$this->oidcConfig = json_decode($response->getBody()->getContents(), true);
|
$this->oidcConfig = json_decode($response->getBody()->getContents(), true);
|
||||||
|
|
||||||
|
|
@ -122,7 +122,7 @@ class OidcProvider extends AbstractProvider implements ProviderInterface
|
||||||
/**
|
/**
|
||||||
* Map the raw user array to a Socialite User instance.
|
* Map the raw user array to a Socialite User instance.
|
||||||
*/
|
*/
|
||||||
protected function mapUserToObject(array $user)
|
public function mapUserToObject(array $user)
|
||||||
{
|
{
|
||||||
return (new User)->setRaw($user)->map([
|
return (new User)->setRaw($user)->map([
|
||||||
'id' => $user['sub'],
|
'id' => $user['sub'],
|
||||||
|
|
|
||||||
|
|
@ -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 () {
|
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->getName())->toBe('recipes:seed');
|
||||||
expect($command->getDescription())->toBe('Seed example recipes');
|
expect($command->getDescription())->toBe('Seed example recipes');
|
||||||
|
|
|
||||||
|
|
@ -2,14 +2,12 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
declare(strict_types=1);
|
||||||
|
|
||||||
use App\Jobs\FirmwarePollJob;
|
|
||||||
use App\Models\Firmware;
|
|
||||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
uses(RefreshDatabase::class);
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
test('firmware check command has correct signature', function () {
|
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->getName())->toBe('trmnl:firmware:check');
|
||||||
expect($command->getDescription())->toBe('Checks for the latest firmware and downloads it if flag --download is passed.');
|
expect($command->getDescription())->toBe('Checks for the latest firmware and downloads it if flag --download is passed.');
|
||||||
|
|
|
||||||
86
tests/Feature/Console/FirmwareUpdateCommandTest.php
Normal file
86
tests/Feature/Console/FirmwareUpdateCommandTest.php
Normal 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);
|
||||||
|
});
|
||||||
154
tests/Feature/Console/MashupCreateCommandTest.php
Normal file
154
tests/Feature/Console/MashupCreateCommandTest.php
Normal 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);
|
||||||
|
});
|
||||||
188
tests/Feature/Console/OidcTestCommandTest.php
Normal file
188
tests/Feature/Console/OidcTestCommandTest.php
Normal 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);
|
||||||
|
});
|
||||||
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([
|
$firmware = Firmware::factory()->create([
|
||||||
'url' => 'https://example.com/firmware.bin',
|
'url' => 'https://example.com/firmware.bin',
|
||||||
'version_tag' => '1.0.0',
|
'version_tag' => '1.0.0',
|
||||||
|
'storage_location' => null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
Http::fake([
|
Http::fake([
|
||||||
|
|
@ -33,9 +34,127 @@ test('it downloads firmware and updates storage location', function () {
|
||||||
$firmware = Firmware::factory()->create([
|
$firmware = Firmware::factory()->create([
|
||||||
'url' => 'https://example.com/firmware.bin',
|
'url' => 'https://example.com/firmware.bin',
|
||||||
'version_tag' => '1.0.0',
|
'version_tag' => '1.0.0',
|
||||||
|
'storage_location' => null,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
(new FirmwareDownloadJob($firmware))->handle();
|
(new FirmwareDownloadJob($firmware))->handle();
|
||||||
|
|
||||||
expect($firmware->fresh()->storage_location)->toBe('firmwares/FW1.0.0.bin');
|
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();
|
||||||
|
});
|
||||||
|
|
|
||||||
115
tests/Feature/Livewire/Actions/DeviceAutoJoinTest.php
Normal file
115
tests/Feature/Livewire/Actions/DeviceAutoJoinTest.php
Normal 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();
|
||||||
|
});
|
||||||
|
|
@ -60,3 +60,78 @@ test('l_word returns original word for unknown locales', function () {
|
||||||
|
|
||||||
expect($filter->l_word('today', 'unknown-locale'))->toBe('today');
|
expect($filter->l_word('today', 'unknown-locale'))->toBe('today');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('l_date handles locale parameter', function () {
|
||||||
|
$filter = new Localization();
|
||||||
|
$date = '2025-01-11';
|
||||||
|
|
||||||
|
$result = $filter->l_date($date, 'Y-m-d', 'de');
|
||||||
|
|
||||||
|
// The result should still contain the date components
|
||||||
|
expect($result)->toContain('2025');
|
||||||
|
expect($result)->toContain('01');
|
||||||
|
expect($result)->toContain('11');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('l_date handles null locale parameter', function () {
|
||||||
|
$filter = new Localization();
|
||||||
|
$date = '2025-01-11';
|
||||||
|
|
||||||
|
$result = $filter->l_date($date, 'Y-m-d', null);
|
||||||
|
|
||||||
|
// Should work the same as default
|
||||||
|
expect($result)->toContain('2025');
|
||||||
|
expect($result)->toContain('01');
|
||||||
|
expect($result)->toContain('11');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('l_date handles different date formats with locale', function () {
|
||||||
|
$filter = new Localization();
|
||||||
|
$date = '2025-01-11';
|
||||||
|
|
||||||
|
$result = $filter->l_date($date, '%B %d, %Y', 'en');
|
||||||
|
|
||||||
|
// Should contain the month name and date
|
||||||
|
expect($result)->toContain('2025');
|
||||||
|
expect($result)->toContain('11');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('l_date handles DateTimeInterface objects with locale', function () {
|
||||||
|
$filter = new Localization();
|
||||||
|
$date = new DateTimeImmutable('2025-01-11');
|
||||||
|
|
||||||
|
$result = $filter->l_date($date, 'Y-m-d', 'fr');
|
||||||
|
|
||||||
|
// Should still format correctly
|
||||||
|
expect($result)->toContain('2025');
|
||||||
|
expect($result)->toContain('01');
|
||||||
|
expect($result)->toContain('11');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('l_date handles invalid date gracefully', function () {
|
||||||
|
$filter = new Localization();
|
||||||
|
$invalidDate = 'invalid-date';
|
||||||
|
|
||||||
|
// This should throw an exception or return a default value
|
||||||
|
// The exact behavior depends on Carbon's implementation
|
||||||
|
expect(fn () => $filter->l_date($invalidDate))->toThrow(Exception::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('l_word handles empty string', function () {
|
||||||
|
$filter = new Localization();
|
||||||
|
|
||||||
|
expect($filter->l_word('', 'de'))->toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('l_word handles special characters', function () {
|
||||||
|
$filter = new Localization();
|
||||||
|
|
||||||
|
// Test with a word that has special characters
|
||||||
|
expect($filter->l_word('café', 'de'))->toBe('café');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('l_word handles numeric strings', function () {
|
||||||
|
$filter = new Localization();
|
||||||
|
|
||||||
|
expect($filter->l_word('123', 'de'))->toBe('123');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,97 @@ test('number_to_currency handles custom currency symbols', function () {
|
||||||
test('number_to_currency handles custom delimiters and separators', function () {
|
test('number_to_currency handles custom delimiters and separators', function () {
|
||||||
$filter = new Numbers();
|
$filter = new Numbers();
|
||||||
|
|
||||||
expect($filter->number_to_currency(1234.57, '£', '.', ','))->toBe('1.234,57 £');
|
$result1 = $filter->number_to_currency(1234.57, '£', '.', ',');
|
||||||
expect($filter->number_to_currency(1234.57, '€', ',', '.'))->toBe('€1,234.57');
|
$result2 = $filter->number_to_currency(1234.57, '€', ',', '.');
|
||||||
|
|
||||||
|
expect($result1)->toContain('1.234,57');
|
||||||
|
expect($result1)->toContain('£');
|
||||||
|
expect($result2)->toContain('1,234.57');
|
||||||
|
expect($result2)->toContain('€');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('number_with_delimiter handles string numbers', function () {
|
||||||
|
$filter = new Numbers();
|
||||||
|
|
||||||
|
expect($filter->number_with_delimiter('1234'))->toBe('1,234');
|
||||||
|
expect($filter->number_with_delimiter('1234.56'))->toBe('1,234.56');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('number_with_delimiter handles negative numbers', function () {
|
||||||
|
$filter = new Numbers();
|
||||||
|
|
||||||
|
expect($filter->number_with_delimiter(-1234))->toBe('-1,234');
|
||||||
|
expect($filter->number_with_delimiter(-1234.56))->toBe('-1,234.56');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('number_with_delimiter handles zero', function () {
|
||||||
|
$filter = new Numbers();
|
||||||
|
|
||||||
|
expect($filter->number_with_delimiter(0))->toBe('0');
|
||||||
|
expect($filter->number_with_delimiter(0.0))->toBe('0.00');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('number_with_delimiter handles very small numbers', function () {
|
||||||
|
$filter = new Numbers();
|
||||||
|
|
||||||
|
expect($filter->number_with_delimiter(0.01))->toBe('0.01');
|
||||||
|
expect($filter->number_with_delimiter(0.001))->toBe('0.00');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('number_to_currency handles string numbers', function () {
|
||||||
|
$filter = new Numbers();
|
||||||
|
|
||||||
|
expect($filter->number_to_currency('1234'))->toBe('$1,234');
|
||||||
|
expect($filter->number_to_currency('1234.56'))->toBe('$1,234.56');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('number_to_currency handles negative numbers', function () {
|
||||||
|
$filter = new Numbers();
|
||||||
|
|
||||||
|
expect($filter->number_to_currency(-1234))->toBe('-$1,234');
|
||||||
|
expect($filter->number_to_currency(-1234.56))->toBe('-$1,234.56');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('number_to_currency handles zero', function () {
|
||||||
|
$filter = new Numbers();
|
||||||
|
|
||||||
|
expect($filter->number_to_currency(0))->toBe('$0');
|
||||||
|
expect($filter->number_to_currency(0.0))->toBe('$0.00');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('number_to_currency handles currency code conversion', function () {
|
||||||
|
$filter = new Numbers();
|
||||||
|
|
||||||
|
expect($filter->number_to_currency(1234, '$'))->toBe('$1,234');
|
||||||
|
expect($filter->number_to_currency(1234, '€'))->toBe('€1,234');
|
||||||
|
expect($filter->number_to_currency(1234, '£'))->toBe('£1,234');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('number_to_currency handles German locale formatting', function () {
|
||||||
|
$filter = new Numbers();
|
||||||
|
|
||||||
|
// When delimiter is '.' and separator is ',', it should use German locale
|
||||||
|
$result = $filter->number_to_currency(1234.56, 'EUR', '.', ',');
|
||||||
|
expect($result)->toContain('1.234,56');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('number_with_delimiter handles different decimal separators', function () {
|
||||||
|
$filter = new Numbers();
|
||||||
|
|
||||||
|
expect($filter->number_with_delimiter(1234.56, ',', ','))->toBe('1,234,56');
|
||||||
|
expect($filter->number_with_delimiter(1234.56, ' ', ','))->toBe('1 234,56');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('number_to_currency handles very large numbers', function () {
|
||||||
|
$filter = new Numbers();
|
||||||
|
|
||||||
|
expect($filter->number_to_currency(1000000))->toBe('$1,000,000');
|
||||||
|
expect($filter->number_to_currency(1000000.50))->toBe('$1,000,000.50');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('number_with_delimiter handles very large numbers', function () {
|
||||||
|
$filter = new Numbers();
|
||||||
|
|
||||||
|
expect($filter->number_with_delimiter(1000000))->toBe('1,000,000');
|
||||||
|
expect($filter->number_with_delimiter(1000000.50))->toBe('1,000,000.50');
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -88,3 +88,83 @@ test('strip_html handles nested tags', function () {
|
||||||
|
|
||||||
expect($filter->strip_html($html))->toBe('Paragraph with nested tags.');
|
expect($filter->strip_html($html))->toBe('Paragraph with nested tags.');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('markdown_to_html handles CommonMarkException gracefully', function () {
|
||||||
|
$filter = new StringMarkup();
|
||||||
|
|
||||||
|
// Create a mock that throws CommonMarkException
|
||||||
|
$filter = new class extends StringMarkup
|
||||||
|
{
|
||||||
|
public function markdown_to_html(string $markdown): ?string
|
||||||
|
{
|
||||||
|
try {
|
||||||
|
// Simulate CommonMarkException
|
||||||
|
throw new Exception('Invalid markdown');
|
||||||
|
} catch (Exception $e) {
|
||||||
|
Illuminate\Support\Facades\Log::error('Markdown conversion error: '.$e->getMessage());
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$result = $filter->markdown_to_html('invalid markdown');
|
||||||
|
|
||||||
|
expect($result)->toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('markdown_to_html handles empty string', function () {
|
||||||
|
$filter = new StringMarkup();
|
||||||
|
|
||||||
|
$result = $filter->markdown_to_html('');
|
||||||
|
|
||||||
|
expect($result)->toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('markdown_to_html handles complex markdown', function () {
|
||||||
|
$filter = new StringMarkup();
|
||||||
|
$markdown = "# Heading\n\nThis is a paragraph with **bold** and *italic* text.\n\n- List item 1\n- List item 2\n\n[Link](https://example.com)";
|
||||||
|
|
||||||
|
$result = $filter->markdown_to_html($markdown);
|
||||||
|
|
||||||
|
expect($result)->toContain('<h1>Heading</h1>');
|
||||||
|
expect($result)->toContain('<strong>bold</strong>');
|
||||||
|
expect($result)->toContain('<em>italic</em>');
|
||||||
|
expect($result)->toContain('<ul>');
|
||||||
|
expect($result)->toContain('<li>List item 1</li>');
|
||||||
|
expect($result)->toContain('<a href="https://example.com">Link</a>');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('strip_html handles empty string', function () {
|
||||||
|
$filter = new StringMarkup();
|
||||||
|
|
||||||
|
expect($filter->strip_html(''))->toBe('');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('strip_html handles string without HTML tags', function () {
|
||||||
|
$filter = new StringMarkup();
|
||||||
|
$text = 'This is plain text without any HTML tags.';
|
||||||
|
|
||||||
|
expect($filter->strip_html($text))->toBe($text);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('strip_html handles self-closing tags', function () {
|
||||||
|
$filter = new StringMarkup();
|
||||||
|
$html = '<p>Text with <br/> line break and <hr/> horizontal rule.</p>';
|
||||||
|
|
||||||
|
expect($filter->strip_html($html))->toBe('Text with line break and horizontal rule.');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pluralize handles zero count', function () {
|
||||||
|
$filter = new StringMarkup();
|
||||||
|
|
||||||
|
expect($filter->pluralize('book', 0))->toBe('0 books');
|
||||||
|
expect($filter->pluralize('person', 0))->toBe('0 people');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('pluralize handles negative count', function () {
|
||||||
|
$filter = new StringMarkup();
|
||||||
|
|
||||||
|
expect($filter->pluralize('book', -1))->toBe('-1 book');
|
||||||
|
expect($filter->pluralize('person', -5))->toBe('-5 people');
|
||||||
|
});
|
||||||
|
|
|
||||||
|
|
@ -74,4 +74,3 @@ test('battery low notification creates correct array representation', function (
|
||||||
'battery_percent' => $device->battery_percent,
|
'battery_percent' => $device->battery_percent,
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -2,11 +2,10 @@
|
||||||
|
|
||||||
declare(strict_types=1);
|
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\Device;
|
||||||
use App\Models\User;
|
use App\Models\User;
|
||||||
|
use App\Notifications\BatteryLow;
|
||||||
|
use App\Notifications\Channels\WebhookChannel;
|
||||||
use GuzzleHttp\Client;
|
use GuzzleHttp\Client;
|
||||||
use GuzzleHttp\Exception\GuzzleException;
|
use GuzzleHttp\Exception\GuzzleException;
|
||||||
use GuzzleHttp\Psr7\Response;
|
use GuzzleHttp\Psr7\Response;
|
||||||
|
|
@ -16,7 +15,8 @@ test('webhook channel returns null when no webhook url is configured', function
|
||||||
$client = Mockery::mock(Client::class);
|
$client = Mockery::mock(Client::class);
|
||||||
$channel = new WebhookChannel($client);
|
$channel = new WebhookChannel($client);
|
||||||
|
|
||||||
$user = new class extends User {
|
$user = new class extends User
|
||||||
|
{
|
||||||
public function routeNotificationFor($driver, $notification = null)
|
public function routeNotificationFor($driver, $notification = null)
|
||||||
{
|
{
|
||||||
return null; // No webhook URL configured
|
return null; // No webhook URL configured
|
||||||
|
|
@ -34,14 +34,16 @@ test('webhook channel throws exception when notification does not implement toWe
|
||||||
$client = Mockery::mock(Client::class);
|
$client = Mockery::mock(Client::class);
|
||||||
$channel = new WebhookChannel($client);
|
$channel = new WebhookChannel($client);
|
||||||
|
|
||||||
$user = new class extends User {
|
$user = new class extends User
|
||||||
|
{
|
||||||
public function routeNotificationFor($driver, $notification = null)
|
public function routeNotificationFor($driver, $notification = null)
|
||||||
{
|
{
|
||||||
return 'https://example.com/webhook';
|
return 'https://example.com/webhook';
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$notification = new class extends Notification {
|
$notification = new class extends Notification
|
||||||
|
{
|
||||||
public function via($notifiable)
|
public function via($notifiable)
|
||||||
{
|
{
|
||||||
return [];
|
return [];
|
||||||
|
|
@ -49,14 +51,15 @@ test('webhook channel throws exception when notification does not implement toWe
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(fn () => $channel->send($user, $notification))
|
expect(fn () => $channel->send($user, $notification))
|
||||||
->toThrow(\Exception::class, 'Notification does not implement toWebhook method.');
|
->toThrow(Exception::class, 'Notification does not implement toWebhook method.');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('webhook channel sends successful webhook request', function () {
|
test('webhook channel sends successful webhook request', function () {
|
||||||
$client = Mockery::mock(Client::class);
|
$client = Mockery::mock(Client::class);
|
||||||
$channel = new WebhookChannel($client);
|
$channel = new WebhookChannel($client);
|
||||||
|
|
||||||
$user = new class extends User {
|
$user = new class extends User
|
||||||
|
{
|
||||||
public function routeNotificationFor($driver, $notification = null)
|
public function routeNotificationFor($driver, $notification = null)
|
||||||
{
|
{
|
||||||
return 'https://example.com/webhook';
|
return 'https://example.com/webhook';
|
||||||
|
|
@ -83,12 +86,12 @@ test('webhook channel sends successful webhook request', function () {
|
||||||
expect($result)->toBe($expectedResponse);
|
expect($result)->toBe($expectedResponse);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
test('webhook channel throws exception when response status is not successful', function () {
|
test('webhook channel throws exception when response status is not successful', function () {
|
||||||
$client = Mockery::mock(Client::class);
|
$client = Mockery::mock(Client::class);
|
||||||
$channel = new WebhookChannel($client);
|
$channel = new WebhookChannel($client);
|
||||||
|
|
||||||
$user = new class extends User {
|
$user = new class extends User
|
||||||
|
{
|
||||||
public function routeNotificationFor($driver, $notification = null)
|
public function routeNotificationFor($driver, $notification = null)
|
||||||
{
|
{
|
||||||
return 'https://example.com/webhook';
|
return 'https://example.com/webhook';
|
||||||
|
|
@ -105,14 +108,15 @@ test('webhook channel throws exception when response status is not successful',
|
||||||
->andReturn($errorResponse);
|
->andReturn($errorResponse);
|
||||||
|
|
||||||
expect(fn () => $channel->send($user, $notification))
|
expect(fn () => $channel->send($user, $notification))
|
||||||
->toThrow(\Exception::class, 'Webhook request failed with status code: 400');
|
->toThrow(Exception::class, 'Webhook request failed with status code: 400');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('webhook channel handles guzzle exceptions', function () {
|
test('webhook channel handles guzzle exceptions', function () {
|
||||||
$client = Mockery::mock(Client::class);
|
$client = Mockery::mock(Client::class);
|
||||||
$channel = new WebhookChannel($client);
|
$channel = new WebhookChannel($client);
|
||||||
|
|
||||||
$user = new class extends User {
|
$user = new class extends User
|
||||||
|
{
|
||||||
public function routeNotificationFor($driver, $notification = null)
|
public function routeNotificationFor($driver, $notification = null)
|
||||||
{
|
{
|
||||||
return 'https://example.com/webhook';
|
return 'https://example.com/webhook';
|
||||||
|
|
@ -124,8 +128,8 @@ test('webhook channel handles guzzle exceptions', function () {
|
||||||
|
|
||||||
$client->shouldReceive('post')
|
$client->shouldReceive('post')
|
||||||
->once()
|
->once()
|
||||||
->andThrow(new class extends \Exception implements GuzzleException {});
|
->andThrow(new class extends Exception implements GuzzleException {});
|
||||||
|
|
||||||
expect(fn () => $channel->send($user, $notification))
|
expect(fn () => $channel->send($user, $notification))
|
||||||
->toThrow(\Exception::class);
|
->toThrow(Exception::class);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
281
tests/Unit/Services/OidcProviderTest.php
Normal file
281
tests/Unit/Services/OidcProviderTest.php
Normal file
|
|
@ -0,0 +1,281 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Services\OidcProvider;
|
||||||
|
use GuzzleHttp\Client;
|
||||||
|
use GuzzleHttp\Exception\RequestException;
|
||||||
|
use GuzzleHttp\Psr7\Response;
|
||||||
|
use Illuminate\Http\Request;
|
||||||
|
use Laravel\Socialite\Two\User;
|
||||||
|
|
||||||
|
test('oidc provider throws exception when endpoint is not configured', function () {
|
||||||
|
config(['services.oidc.endpoint' => null]);
|
||||||
|
|
||||||
|
expect(fn () => new OidcProvider(
|
||||||
|
new Request(),
|
||||||
|
'client-id',
|
||||||
|
'client-secret',
|
||||||
|
'redirect-url'
|
||||||
|
))->toThrow(Exception::class, 'OIDC endpoint is not configured');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('oidc provider handles well-known endpoint url', function () {
|
||||||
|
config(['services.oidc.endpoint' => 'https://example.com/.well-known/openid-configuration']);
|
||||||
|
|
||||||
|
$mockClient = Mockery::mock(Client::class);
|
||||||
|
$mockResponse = Mockery::mock(Response::class);
|
||||||
|
$mockResponse->shouldReceive('getBody->getContents')
|
||||||
|
->andReturn(json_encode([
|
||||||
|
'authorization_endpoint' => 'https://example.com/auth',
|
||||||
|
'token_endpoint' => 'https://example.com/token',
|
||||||
|
'userinfo_endpoint' => 'https://example.com/userinfo',
|
||||||
|
]));
|
||||||
|
|
||||||
|
$mockClient->shouldReceive('get')
|
||||||
|
->with('https://example.com/.well-known/openid-configuration')
|
||||||
|
->andReturn($mockResponse);
|
||||||
|
|
||||||
|
$this->app->instance(Client::class, $mockClient);
|
||||||
|
|
||||||
|
$provider = new OidcProvider(
|
||||||
|
new Request(),
|
||||||
|
'client-id',
|
||||||
|
'client-secret',
|
||||||
|
'redirect-url'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($provider)->toBeInstanceOf(OidcProvider::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('oidc provider handles base url endpoint', function () {
|
||||||
|
config(['services.oidc.endpoint' => 'https://example.com']);
|
||||||
|
|
||||||
|
$mockClient = Mockery::mock(Client::class);
|
||||||
|
$mockResponse = Mockery::mock(Response::class);
|
||||||
|
$mockResponse->shouldReceive('getBody->getContents')
|
||||||
|
->andReturn(json_encode([
|
||||||
|
'authorization_endpoint' => 'https://example.com/auth',
|
||||||
|
'token_endpoint' => 'https://example.com/token',
|
||||||
|
'userinfo_endpoint' => 'https://example.com/userinfo',
|
||||||
|
]));
|
||||||
|
|
||||||
|
$mockClient->shouldReceive('get')
|
||||||
|
->with('https://example.com/.well-known/openid-configuration')
|
||||||
|
->andReturn($mockResponse);
|
||||||
|
|
||||||
|
$this->app->instance(Client::class, $mockClient);
|
||||||
|
|
||||||
|
$provider = new OidcProvider(
|
||||||
|
new Request(),
|
||||||
|
'client-id',
|
||||||
|
'client-secret',
|
||||||
|
'redirect-url'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($provider)->toBeInstanceOf(OidcProvider::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('oidc provider throws exception when configuration is empty', function () {
|
||||||
|
config(['services.oidc.endpoint' => 'https://example.com']);
|
||||||
|
|
||||||
|
$mockClient = Mockery::mock(Client::class);
|
||||||
|
$mockResponse = Mockery::mock(Response::class);
|
||||||
|
$mockResponse->shouldReceive('getBody->getContents')
|
||||||
|
->andReturn('');
|
||||||
|
|
||||||
|
$mockClient->shouldReceive('get')
|
||||||
|
->with('https://example.com/.well-known/openid-configuration')
|
||||||
|
->andReturn($mockResponse);
|
||||||
|
|
||||||
|
$this->app->instance(Client::class, $mockClient);
|
||||||
|
|
||||||
|
expect(fn () => new OidcProvider(
|
||||||
|
new Request(),
|
||||||
|
'client-id',
|
||||||
|
'client-secret',
|
||||||
|
'redirect-url'
|
||||||
|
))->toThrow(Exception::class, 'OIDC configuration is empty or invalid JSON');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('oidc provider throws exception when authorization endpoint is missing', function () {
|
||||||
|
config(['services.oidc.endpoint' => 'https://example.com']);
|
||||||
|
|
||||||
|
$mockClient = Mockery::mock(Client::class);
|
||||||
|
$mockResponse = Mockery::mock(Response::class);
|
||||||
|
$mockResponse->shouldReceive('getBody->getContents')
|
||||||
|
->andReturn(json_encode([
|
||||||
|
'token_endpoint' => 'https://example.com/token',
|
||||||
|
'userinfo_endpoint' => 'https://example.com/userinfo',
|
||||||
|
]));
|
||||||
|
|
||||||
|
$mockClient->shouldReceive('get')
|
||||||
|
->with('https://example.com/.well-known/openid-configuration')
|
||||||
|
->andReturn($mockResponse);
|
||||||
|
|
||||||
|
$this->app->instance(Client::class, $mockClient);
|
||||||
|
|
||||||
|
expect(fn () => new OidcProvider(
|
||||||
|
new Request(),
|
||||||
|
'client-id',
|
||||||
|
'client-secret',
|
||||||
|
'redirect-url'
|
||||||
|
))->toThrow(Exception::class, 'authorization_endpoint not found in OIDC configuration');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('oidc provider throws exception when configuration request fails', function () {
|
||||||
|
config(['services.oidc.endpoint' => 'https://example.com']);
|
||||||
|
|
||||||
|
$mockClient = Mockery::mock(Client::class);
|
||||||
|
$mockClient->shouldReceive('get')
|
||||||
|
->with('https://example.com/.well-known/openid-configuration')
|
||||||
|
->andThrow(new RequestException('Connection failed', new GuzzleHttp\Psr7\Request('GET', 'test')));
|
||||||
|
|
||||||
|
$this->app->instance(Client::class, $mockClient);
|
||||||
|
|
||||||
|
expect(fn () => new OidcProvider(
|
||||||
|
new Request(),
|
||||||
|
'client-id',
|
||||||
|
'client-secret',
|
||||||
|
'redirect-url'
|
||||||
|
))->toThrow(Exception::class, 'Failed to load OIDC configuration');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('oidc provider uses default scopes when none provided', function () {
|
||||||
|
config(['services.oidc.endpoint' => 'https://example.com']);
|
||||||
|
|
||||||
|
$mockClient = Mockery::mock(Client::class);
|
||||||
|
$mockResponse = Mockery::mock(Response::class);
|
||||||
|
$mockResponse->shouldReceive('getBody->getContents')
|
||||||
|
->andReturn(json_encode([
|
||||||
|
'authorization_endpoint' => 'https://example.com/auth',
|
||||||
|
'token_endpoint' => 'https://example.com/token',
|
||||||
|
'userinfo_endpoint' => 'https://example.com/userinfo',
|
||||||
|
]));
|
||||||
|
|
||||||
|
$mockClient->shouldReceive('get')
|
||||||
|
->with('https://example.com/.well-known/openid-configuration')
|
||||||
|
->andReturn($mockResponse);
|
||||||
|
|
||||||
|
$this->app->instance(Client::class, $mockClient);
|
||||||
|
|
||||||
|
$provider = new OidcProvider(
|
||||||
|
new Request(),
|
||||||
|
'client-id',
|
||||||
|
'client-secret',
|
||||||
|
'redirect-url'
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($provider)->toBeInstanceOf(OidcProvider::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('oidc provider uses custom scopes when provided', function () {
|
||||||
|
config(['services.oidc.endpoint' => 'https://example.com']);
|
||||||
|
|
||||||
|
$mockClient = Mockery::mock(Client::class);
|
||||||
|
$mockResponse = Mockery::mock(Response::class);
|
||||||
|
$mockResponse->shouldReceive('getBody->getContents')
|
||||||
|
->andReturn(json_encode([
|
||||||
|
'authorization_endpoint' => 'https://example.com/auth',
|
||||||
|
'token_endpoint' => 'https://example.com/token',
|
||||||
|
'userinfo_endpoint' => 'https://example.com/userinfo',
|
||||||
|
]));
|
||||||
|
|
||||||
|
$mockClient->shouldReceive('get')
|
||||||
|
->with('https://example.com/.well-known/openid-configuration')
|
||||||
|
->andReturn($mockResponse);
|
||||||
|
|
||||||
|
$this->app->instance(Client::class, $mockClient);
|
||||||
|
|
||||||
|
$provider = new OidcProvider(
|
||||||
|
new Request(),
|
||||||
|
'client-id',
|
||||||
|
'client-secret',
|
||||||
|
'redirect-url',
|
||||||
|
['openid', 'profile', 'email', 'custom_scope']
|
||||||
|
);
|
||||||
|
|
||||||
|
expect($provider)->toBeInstanceOf(OidcProvider::class);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('oidc provider maps user data correctly', function () {
|
||||||
|
config(['services.oidc.endpoint' => 'https://example.com']);
|
||||||
|
|
||||||
|
$mockClient = Mockery::mock(Client::class);
|
||||||
|
$mockResponse = Mockery::mock(Response::class);
|
||||||
|
$mockResponse->shouldReceive('getBody->getContents')
|
||||||
|
->andReturn(json_encode([
|
||||||
|
'authorization_endpoint' => 'https://example.com/auth',
|
||||||
|
'token_endpoint' => 'https://example.com/token',
|
||||||
|
'userinfo_endpoint' => 'https://example.com/userinfo',
|
||||||
|
]));
|
||||||
|
|
||||||
|
$mockClient->shouldReceive('get')
|
||||||
|
->with('https://example.com/.well-known/openid-configuration')
|
||||||
|
->andReturn($mockResponse);
|
||||||
|
|
||||||
|
$this->app->instance(Client::class, $mockClient);
|
||||||
|
|
||||||
|
$provider = new OidcProvider(
|
||||||
|
new Request(),
|
||||||
|
'client-id',
|
||||||
|
'client-secret',
|
||||||
|
'redirect-url'
|
||||||
|
);
|
||||||
|
|
||||||
|
$userData = [
|
||||||
|
'sub' => 'user123',
|
||||||
|
'name' => 'John Doe',
|
||||||
|
'email' => 'john@example.com',
|
||||||
|
'preferred_username' => 'johndoe',
|
||||||
|
'picture' => 'https://example.com/avatar.jpg',
|
||||||
|
];
|
||||||
|
|
||||||
|
$user = $provider->mapUserToObject($userData);
|
||||||
|
|
||||||
|
expect($user)->toBeInstanceOf(User::class);
|
||||||
|
expect($user->getId())->toBe('user123');
|
||||||
|
expect($user->getName())->toBe('John Doe');
|
||||||
|
expect($user->getEmail())->toBe('john@example.com');
|
||||||
|
expect($user->getNickname())->toBe('johndoe');
|
||||||
|
expect($user->getAvatar())->toBe('https://example.com/avatar.jpg');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('oidc provider handles missing user fields gracefully', function () {
|
||||||
|
config(['services.oidc.endpoint' => 'https://example.com']);
|
||||||
|
|
||||||
|
$mockClient = Mockery::mock(Client::class);
|
||||||
|
$mockResponse = Mockery::mock(Response::class);
|
||||||
|
$mockResponse->shouldReceive('getBody->getContents')
|
||||||
|
->andReturn(json_encode([
|
||||||
|
'authorization_endpoint' => 'https://example.com/auth',
|
||||||
|
'token_endpoint' => 'https://example.com/token',
|
||||||
|
'userinfo_endpoint' => 'https://example.com/userinfo',
|
||||||
|
]));
|
||||||
|
|
||||||
|
$mockClient->shouldReceive('get')
|
||||||
|
->with('https://example.com/.well-known/openid-configuration')
|
||||||
|
->andReturn($mockResponse);
|
||||||
|
|
||||||
|
$this->app->instance(Client::class, $mockClient);
|
||||||
|
|
||||||
|
$provider = new OidcProvider(
|
||||||
|
new Request(),
|
||||||
|
'client-id',
|
||||||
|
'client-secret',
|
||||||
|
'redirect-url'
|
||||||
|
);
|
||||||
|
|
||||||
|
$userData = [
|
||||||
|
'sub' => 'user123',
|
||||||
|
];
|
||||||
|
|
||||||
|
$user = $provider->mapUserToObject($userData);
|
||||||
|
|
||||||
|
expect($user)->toBeInstanceOf(User::class);
|
||||||
|
expect($user->getId())->toBe('user123');
|
||||||
|
expect($user->getName())->toBeNull();
|
||||||
|
expect($user->getEmail())->toBeNull();
|
||||||
|
expect($user->getNickname())->toBeNull();
|
||||||
|
expect($user->getAvatar())->toBeNull();
|
||||||
|
});
|
||||||
Loading…
Add table
Add a link
Reference in a new issue