mirror of
https://github.com/usetrmnl/byos_laravel.git
synced 2026-01-13 23:18:10 +00:00
This commit is contained in:
parent
4f251bf37e
commit
42b515e322
21 changed files with 2212 additions and 32 deletions
|
|
@ -60,3 +60,78 @@ test('l_word returns original word for unknown locales', function () {
|
|||
|
||||
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 () {
|
||||
$filter = new Numbers();
|
||||
|
||||
expect($filter->number_to_currency(1234.57, '£', '.', ','))->toBe('1.234,57 £');
|
||||
expect($filter->number_to_currency(1234.57, '€', ',', '.'))->toBe('€1,234.57');
|
||||
$result1 = $filter->number_to_currency(1234.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.');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
|
|
|
|||
119
tests/Unit/Models/DeviceModelTest.php
Normal file
119
tests/Unit/Models/DeviceModelTest.php
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\DeviceModel;
|
||||
|
||||
test('device model has required attributes', function () {
|
||||
$deviceModel = DeviceModel::factory()->create([
|
||||
'name' => 'Test Model',
|
||||
'width' => 800,
|
||||
'height' => 480,
|
||||
'colors' => 4,
|
||||
'bit_depth' => 2,
|
||||
'scale_factor' => 1.0,
|
||||
'rotation' => 0,
|
||||
'offset_x' => 0,
|
||||
'offset_y' => 0,
|
||||
]);
|
||||
|
||||
expect($deviceModel->name)->toBe('Test Model');
|
||||
expect($deviceModel->width)->toBe(800);
|
||||
expect($deviceModel->height)->toBe(480);
|
||||
expect($deviceModel->colors)->toBe(4);
|
||||
expect($deviceModel->bit_depth)->toBe(2);
|
||||
expect($deviceModel->scale_factor)->toBe(1.0);
|
||||
expect($deviceModel->rotation)->toBe(0);
|
||||
expect($deviceModel->offset_x)->toBe(0);
|
||||
expect($deviceModel->offset_y)->toBe(0);
|
||||
});
|
||||
|
||||
test('device model casts attributes correctly', function () {
|
||||
$deviceModel = DeviceModel::factory()->create([
|
||||
'width' => '800',
|
||||
'height' => '480',
|
||||
'colors' => '4',
|
||||
'bit_depth' => '2',
|
||||
'scale_factor' => '1.5',
|
||||
'rotation' => '90',
|
||||
'offset_x' => '10',
|
||||
'offset_y' => '20',
|
||||
]);
|
||||
|
||||
expect($deviceModel->width)->toBeInt();
|
||||
expect($deviceModel->height)->toBeInt();
|
||||
expect($deviceModel->colors)->toBeInt();
|
||||
expect($deviceModel->bit_depth)->toBeInt();
|
||||
expect($deviceModel->scale_factor)->toBeFloat();
|
||||
expect($deviceModel->rotation)->toBeInt();
|
||||
expect($deviceModel->offset_x)->toBeInt();
|
||||
expect($deviceModel->offset_y)->toBeInt();
|
||||
});
|
||||
|
||||
test('get color depth attribute returns correct format for bit depth 2', function () {
|
||||
$deviceModel = DeviceModel::factory()->create(['bit_depth' => 2]);
|
||||
|
||||
expect($deviceModel->getColorDepthAttribute())->toBe('2bit');
|
||||
});
|
||||
|
||||
test('get color depth attribute returns correct format for bit depth 4', function () {
|
||||
$deviceModel = DeviceModel::factory()->create(['bit_depth' => 4]);
|
||||
|
||||
expect($deviceModel->getColorDepthAttribute())->toBe('4bit');
|
||||
});
|
||||
|
||||
test('get color depth attribute returns 4bit for bit depth greater than 4', function () {
|
||||
$deviceModel = DeviceModel::factory()->create(['bit_depth' => 8]);
|
||||
|
||||
expect($deviceModel->getColorDepthAttribute())->toBe('4bit');
|
||||
});
|
||||
|
||||
test('get color depth attribute returns null when bit depth is null', function () {
|
||||
$deviceModel = new DeviceModel(['bit_depth' => null]);
|
||||
|
||||
expect($deviceModel->getColorDepthAttribute())->toBeNull();
|
||||
});
|
||||
|
||||
test('get scale level attribute returns null for width 800 or less', function () {
|
||||
$deviceModel = DeviceModel::factory()->create(['width' => 800]);
|
||||
|
||||
expect($deviceModel->getScaleLevelAttribute())->toBeNull();
|
||||
});
|
||||
|
||||
test('get scale level attribute returns large for width between 801 and 1000', function () {
|
||||
$deviceModel = DeviceModel::factory()->create(['width' => 900]);
|
||||
|
||||
expect($deviceModel->getScaleLevelAttribute())->toBe('large');
|
||||
});
|
||||
|
||||
test('get scale level attribute returns xlarge for width between 1001 and 1400', function () {
|
||||
$deviceModel = DeviceModel::factory()->create(['width' => 1200]);
|
||||
|
||||
expect($deviceModel->getScaleLevelAttribute())->toBe('xlarge');
|
||||
});
|
||||
|
||||
test('get scale level attribute returns xxlarge for width greater than 1400', function () {
|
||||
$deviceModel = DeviceModel::factory()->create(['width' => 1500]);
|
||||
|
||||
expect($deviceModel->getScaleLevelAttribute())->toBe('xxlarge');
|
||||
});
|
||||
|
||||
test('get scale level attribute returns null when width is null', function () {
|
||||
$deviceModel = new DeviceModel(['width' => null]);
|
||||
|
||||
expect($deviceModel->getScaleLevelAttribute())->toBeNull();
|
||||
});
|
||||
|
||||
test('device model factory creates valid data', function () {
|
||||
$deviceModel = DeviceModel::factory()->create();
|
||||
|
||||
expect($deviceModel->name)->not->toBeEmpty();
|
||||
expect($deviceModel->width)->toBeInt();
|
||||
expect($deviceModel->height)->toBeInt();
|
||||
expect($deviceModel->colors)->toBeInt();
|
||||
expect($deviceModel->bit_depth)->toBeInt();
|
||||
expect($deviceModel->scale_factor)->toBeFloat();
|
||||
expect($deviceModel->rotation)->toBeInt();
|
||||
expect($deviceModel->offset_x)->toBeInt();
|
||||
expect($deviceModel->offset_y)->toBeInt();
|
||||
});
|
||||
76
tests/Unit/Notifications/BatteryLowTest.php
Normal file
76
tests/Unit/Notifications/BatteryLowTest.php
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Device;
|
||||
use App\Models\User;
|
||||
use App\Notifications\BatteryLow;
|
||||
use App\Notifications\Channels\WebhookChannel;
|
||||
use Illuminate\Notifications\Messages\MailMessage;
|
||||
|
||||
test('battery low notification has correct via channels', function () {
|
||||
$device = Device::factory()->create();
|
||||
$notification = new BatteryLow($device);
|
||||
|
||||
expect($notification->via(new User()))->toBe(['mail', WebhookChannel::class]);
|
||||
});
|
||||
|
||||
test('battery low notification creates correct mail message', function () {
|
||||
$device = Device::factory()->create([
|
||||
'name' => 'Test Device',
|
||||
'last_battery_voltage' => 3.0,
|
||||
]);
|
||||
|
||||
$notification = new BatteryLow($device);
|
||||
$mailMessage = $notification->toMail(new User());
|
||||
|
||||
expect($mailMessage)->toBeInstanceOf(MailMessage::class);
|
||||
expect($mailMessage->markdown)->toBe('mail.battery-low');
|
||||
expect($mailMessage->viewData['device'])->toBe($device);
|
||||
});
|
||||
|
||||
test('battery low notification creates correct webhook message', function () {
|
||||
config([
|
||||
'services.webhook.notifications.topic' => 'battery.low',
|
||||
'app.name' => 'Test App',
|
||||
]);
|
||||
|
||||
$device = Device::factory()->create([
|
||||
'name' => 'Test Device',
|
||||
'last_battery_voltage' => 3.0,
|
||||
]);
|
||||
|
||||
$notification = new BatteryLow($device);
|
||||
$webhookMessage = $notification->toWebhook(new User());
|
||||
|
||||
expect($webhookMessage->toArray())->toBe([
|
||||
'query' => null,
|
||||
'data' => [
|
||||
'topic' => 'battery.low',
|
||||
'message' => "Battery below {$device->battery_percent}% on device: Test Device",
|
||||
'device_id' => $device->id,
|
||||
'device_name' => 'Test Device',
|
||||
'battery_percent' => $device->battery_percent,
|
||||
],
|
||||
'headers' => [
|
||||
'User-Agent' => 'Test App',
|
||||
'X-TrmnlByos-Event' => 'battery.low',
|
||||
],
|
||||
'verify' => false,
|
||||
]);
|
||||
});
|
||||
|
||||
test('battery low notification creates correct array representation', function () {
|
||||
$device = Device::factory()->create([
|
||||
'name' => 'Test Device',
|
||||
'last_battery_voltage' => 3.0,
|
||||
]);
|
||||
|
||||
$notification = new BatteryLow($device);
|
||||
$array = $notification->toArray(new User());
|
||||
|
||||
expect($array)->toBe([
|
||||
'device_name' => 'Test Device',
|
||||
'battery_percent' => $device->battery_percent,
|
||||
]);
|
||||
});
|
||||
135
tests/Unit/Notifications/WebhookChannelTest.php
Normal file
135
tests/Unit/Notifications/WebhookChannelTest.php
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\Device;
|
||||
use App\Models\User;
|
||||
use App\Notifications\BatteryLow;
|
||||
use App\Notifications\Channels\WebhookChannel;
|
||||
use GuzzleHttp\Client;
|
||||
use GuzzleHttp\Exception\GuzzleException;
|
||||
use GuzzleHttp\Psr7\Response;
|
||||
use Illuminate\Notifications\Notification;
|
||||
|
||||
test('webhook channel returns null when no webhook url is configured', function () {
|
||||
$client = Mockery::mock(Client::class);
|
||||
$channel = new WebhookChannel($client);
|
||||
|
||||
$user = new class extends User
|
||||
{
|
||||
public function routeNotificationFor($driver, $notification = null)
|
||||
{
|
||||
return null; // No webhook URL configured
|
||||
}
|
||||
};
|
||||
|
||||
$notification = new BatteryLow(Device::factory()->create());
|
||||
|
||||
$result = $channel->send($user, $notification);
|
||||
|
||||
expect($result)->toBeNull();
|
||||
});
|
||||
|
||||
test('webhook channel throws exception when notification does not implement toWebhook', function () {
|
||||
$client = Mockery::mock(Client::class);
|
||||
$channel = new WebhookChannel($client);
|
||||
|
||||
$user = new class extends User
|
||||
{
|
||||
public function routeNotificationFor($driver, $notification = null)
|
||||
{
|
||||
return 'https://example.com/webhook';
|
||||
}
|
||||
};
|
||||
|
||||
$notification = new class extends Notification
|
||||
{
|
||||
public function via($notifiable)
|
||||
{
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
expect(fn () => $channel->send($user, $notification))
|
||||
->toThrow(Exception::class, 'Notification does not implement toWebhook method.');
|
||||
});
|
||||
|
||||
test('webhook channel sends successful webhook request', function () {
|
||||
$client = Mockery::mock(Client::class);
|
||||
$channel = new WebhookChannel($client);
|
||||
|
||||
$user = new class extends User
|
||||
{
|
||||
public function routeNotificationFor($driver, $notification = null)
|
||||
{
|
||||
return 'https://example.com/webhook';
|
||||
}
|
||||
};
|
||||
|
||||
$device = Device::factory()->create();
|
||||
$notification = new BatteryLow($device);
|
||||
|
||||
$expectedResponse = new Response(200, [], 'OK');
|
||||
|
||||
$client->shouldReceive('post')
|
||||
->once()
|
||||
->with('https://example.com/webhook', [
|
||||
'query' => null,
|
||||
'body' => json_encode($notification->toWebhook($user)->toArray()['data']),
|
||||
'verify' => false,
|
||||
'headers' => $notification->toWebhook($user)->toArray()['headers'],
|
||||
])
|
||||
->andReturn($expectedResponse);
|
||||
|
||||
$result = $channel->send($user, $notification);
|
||||
|
||||
expect($result)->toBe($expectedResponse);
|
||||
});
|
||||
|
||||
test('webhook channel throws exception when response status is not successful', function () {
|
||||
$client = Mockery::mock(Client::class);
|
||||
$channel = new WebhookChannel($client);
|
||||
|
||||
$user = new class extends User
|
||||
{
|
||||
public function routeNotificationFor($driver, $notification = null)
|
||||
{
|
||||
return 'https://example.com/webhook';
|
||||
}
|
||||
};
|
||||
|
||||
$device = Device::factory()->create();
|
||||
$notification = new BatteryLow($device);
|
||||
|
||||
$errorResponse = new Response(400, [], 'Bad Request');
|
||||
|
||||
$client->shouldReceive('post')
|
||||
->once()
|
||||
->andReturn($errorResponse);
|
||||
|
||||
expect(fn () => $channel->send($user, $notification))
|
||||
->toThrow(Exception::class, 'Webhook request failed with status code: 400');
|
||||
});
|
||||
|
||||
test('webhook channel handles guzzle exceptions', function () {
|
||||
$client = Mockery::mock(Client::class);
|
||||
$channel = new WebhookChannel($client);
|
||||
|
||||
$user = new class extends User
|
||||
{
|
||||
public function routeNotificationFor($driver, $notification = null)
|
||||
{
|
||||
return 'https://example.com/webhook';
|
||||
}
|
||||
};
|
||||
|
||||
$device = Device::factory()->create();
|
||||
$notification = new BatteryLow($device);
|
||||
|
||||
$client->shouldReceive('post')
|
||||
->once()
|
||||
->andThrow(new class extends Exception implements GuzzleException {});
|
||||
|
||||
expect(fn () => $channel->send($user, $notification))
|
||||
->toThrow(Exception::class);
|
||||
});
|
||||
92
tests/Unit/Notifications/WebhookMessageTest.php
Normal file
92
tests/Unit/Notifications/WebhookMessageTest.php
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Notifications\Messages\WebhookMessage;
|
||||
|
||||
test('webhook message can be created with static method', function () {
|
||||
$message = WebhookMessage::create('test data');
|
||||
|
||||
expect($message)->toBeInstanceOf(WebhookMessage::class);
|
||||
});
|
||||
|
||||
test('webhook message can be created with constructor', function () {
|
||||
$message = new WebhookMessage('test data');
|
||||
|
||||
expect($message)->toBeInstanceOf(WebhookMessage::class);
|
||||
});
|
||||
|
||||
test('webhook message can set query parameters', function () {
|
||||
$message = WebhookMessage::create()
|
||||
->query(['param1' => 'value1', 'param2' => 'value2']);
|
||||
|
||||
expect($message->toArray()['query'])->toBe(['param1' => 'value1', 'param2' => 'value2']);
|
||||
});
|
||||
|
||||
test('webhook message can set data', function () {
|
||||
$data = ['key' => 'value', 'nested' => ['array' => 'data']];
|
||||
$message = WebhookMessage::create()
|
||||
->data($data);
|
||||
|
||||
expect($message->toArray()['data'])->toBe($data);
|
||||
});
|
||||
|
||||
test('webhook message can add headers', function () {
|
||||
$message = WebhookMessage::create()
|
||||
->header('X-Custom-Header', 'custom-value')
|
||||
->header('Authorization', 'Bearer token');
|
||||
|
||||
$headers = $message->toArray()['headers'];
|
||||
expect($headers['X-Custom-Header'])->toBe('custom-value');
|
||||
expect($headers['Authorization'])->toBe('Bearer token');
|
||||
});
|
||||
|
||||
test('webhook message can set user agent', function () {
|
||||
$message = WebhookMessage::create()
|
||||
->userAgent('Test App/1.0');
|
||||
|
||||
$headers = $message->toArray()['headers'];
|
||||
expect($headers['User-Agent'])->toBe('Test App/1.0');
|
||||
});
|
||||
|
||||
test('webhook message can set verify option', function () {
|
||||
$message = WebhookMessage::create()
|
||||
->verify(true);
|
||||
|
||||
expect($message->toArray()['verify'])->toBeTrue();
|
||||
});
|
||||
|
||||
test('webhook message verify defaults to false', function () {
|
||||
$message = WebhookMessage::create();
|
||||
|
||||
expect($message->toArray()['verify'])->toBeFalse();
|
||||
});
|
||||
|
||||
test('webhook message can chain methods', function () {
|
||||
$message = WebhookMessage::create(['initial' => 'data'])
|
||||
->query(['param' => 'value'])
|
||||
->data(['updated' => 'data'])
|
||||
->header('X-Test', 'header')
|
||||
->userAgent('Test Agent')
|
||||
->verify(true);
|
||||
|
||||
$array = $message->toArray();
|
||||
|
||||
expect($array['query'])->toBe(['param' => 'value']);
|
||||
expect($array['data'])->toBe(['updated' => 'data']);
|
||||
expect($array['headers']['X-Test'])->toBe('header');
|
||||
expect($array['headers']['User-Agent'])->toBe('Test Agent');
|
||||
expect($array['verify'])->toBeTrue();
|
||||
});
|
||||
|
||||
test('webhook message toArray returns correct structure', function () {
|
||||
$message = WebhookMessage::create(['test' => 'data']);
|
||||
|
||||
$array = $message->toArray();
|
||||
|
||||
expect($array)->toHaveKeys(['query', 'data', 'headers', 'verify']);
|
||||
expect($array['query'])->toBeNull();
|
||||
expect($array['data'])->toBe(['test' => 'data']);
|
||||
expect($array['headers'])->toBeNull();
|
||||
expect($array['verify'])->toBeFalse();
|
||||
});
|
||||
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