diff --git a/app/Jobs/BackupTasks/SendPushoverNotificationJob.php b/app/Jobs/BackupTasks/SendPushoverNotificationJob.php new file mode 100644 index 00000000..969bf33e --- /dev/null +++ b/app/Jobs/BackupTasks/SendPushoverNotificationJob.php @@ -0,0 +1,52 @@ +backupTask->sendPushoverNotification($this->backupTaskLog, $this->notificationStreamValue); + } +} diff --git a/app/Livewire/NotificationStreams/Forms/NotificationStreamForm.php b/app/Livewire/NotificationStreams/Forms/NotificationStreamForm.php index 2a5bc516..945df462 100644 --- a/app/Livewire/NotificationStreams/Forms/NotificationStreamForm.php +++ b/app/Livewire/NotificationStreams/Forms/NotificationStreamForm.php @@ -74,6 +74,7 @@ public function initialize(): void NotificationStream::TYPE_SLACK => __('Slack Webhook'), NotificationStream::TYPE_TEAMS => __('Microsoft Teams Webhook'), NotificationStream::TYPE_EMAIL => __('Email'), + NotificationStream::TYPE_PUSHOVER => __('Pushover'), ]); } @@ -95,6 +96,7 @@ protected function getValueValidationRule(): array|string NotificationStream::TYPE_DISCORD => ['url', 'regex:/^https:\/\/discord\.com\/api\/webhooks\//'], NotificationStream::TYPE_SLACK => ['url', 'regex:/^https:\/\/hooks\.slack\.com\/services\//'], NotificationStream::TYPE_TEAMS => ['url', 'regex:/^https:\/\/.*\.webhook\.office\.com\/webhookb2\/.+/i'], + NotificationStream::TYPE_PUSHOVER => ['string'], NotificationStream::TYPE_EMAIL => ['email'], default => 'string', }; @@ -107,6 +109,7 @@ protected function getValueErrorMessage(): string NotificationStream::TYPE_SLACK => __('Please enter a Slack webhook URL.'), NotificationStream::TYPE_TEAMS => __('Please enter a Microsoft Teams Webhook URL.'), NotificationStream::TYPE_EMAIL => __('Please enter an email address.'), + NotificationStream::TYPE_PUSHOVER => __('Please enter a Pushover token.'), default => __('Please enter a valid value for the selected notification type.'), }; } diff --git a/app/Models/BackupTask.php b/app/Models/BackupTask.php index 989ffc97..2df86582 100644 --- a/app/Models/BackupTask.php +++ b/app/Models/BackupTask.php @@ -5,6 +5,7 @@ namespace App\Models; use App\Jobs\BackupTasks\SendDiscordNotificationJob; +use App\Jobs\BackupTasks\SendPushoverNotificationJob; use App\Jobs\BackupTasks\SendSlackNotificationJob; use App\Jobs\BackupTasks\SendTeamsNotificationJob; use App\Jobs\RunDatabaseBackupTaskJob; @@ -257,8 +258,6 @@ private static function formatFileSize(int $bytes): string return Number::fileSize($bytes); } - // ... [keeping existing static methods] ... - /** * Scope query to include only non-paused backup tasks. * @@ -645,6 +644,16 @@ public function hasTeamNotification(): bool ->exists(); } + /** + * Check if the task has Pushover notifications enabled. + */ + public function hasPushoverNotification(): bool + { + return $this->notificationStreams() + ->where('type', NotificationStream::TYPE_PUSHOVER) + ->exists(); + } + /** * Send notifications for the latest backup task log. * @@ -702,6 +711,7 @@ public function dispatchNotification(NotificationStream $notificationStream, Bac NotificationStream::TYPE_DISCORD => SendDiscordNotificationJob::dispatch($this, $backupTaskLog, $streamValue)->onQueue($queue), NotificationStream::TYPE_SLACK => SendSlackNotificationJob::dispatch($this, $backupTaskLog, $streamValue)->onQueue($queue), NotificationStream::TYPE_TEAMS => SendTeamsNotificationJob::dispatch($this, $backupTaskLog, $streamValue)->onQueue($queue), + NotificationStream::TYPE_PUSHOVER => SendPushoverNotificationJob::dispatch($this, $backupTaskLog, $streamValue)->onQueue($queue), default => throw new InvalidArgumentException("Unsupported notification type: {$notificationStream->getAttribute('type')}"), }; } @@ -898,6 +908,40 @@ public function sendTeamsWebhookNotification(BackupTaskLog $backupTaskLog, strin } } + /** + * Send a Pushover notification for a backup task. + * + * @param BackupTaskLog $backupTaskLog The log entry for the backup task + * @param string $pushoverToken The Pushover API token + */ + public function sendPushoverNotification(BackupTaskLog $backupTaskLog, string $pushoverToken): void + { + $isSuccessful = $backupTaskLog->getAttribute('successful_at') !== null; + $status = $isSuccessful ? 'success' : 'failure'; + $message = $isSuccessful + ? 'The backup task was successful.' + : 'The backup task failed.'; + $priority = $isSuccessful ? 0 : 1; // Normal priority for success, high priority for failure + + $payload = [ + 'token' => $pushoverToken, + 'title' => "{$this->label} Backup Task: " . ucfirst($status), + 'message' => $message . " Details:\n" . + 'Backup Type: ' . ucfirst($this->type) . "\n" . + 'Remote Server: ' . ($this->remoteServer?->label ?? 'N/A') . "\n" . + 'Backup Destination: ' . ($this->backupDestination?->label ?? 'N/A') . + ' (' . ($this->backupDestination?->type() ?? 'N/A') . ")\n" . + 'Ran at: ' . Carbon::parse($backupTaskLog->getAttribute('created_at'))->format('jS F Y, H:i:s'), + 'priority' => $priority, + ]; + + $response = Http::post('https://api.pushover.net/1/messages.json', $payload); + + if (! $response->successful()) { + throw new RuntimeException('Pushover notification failed: ' . $response->body()); + } + } + /** * Check if the task has a custom store path. */ diff --git a/app/Models/NotificationStream.php b/app/Models/NotificationStream.php index 349280e0..7093ab6b 100644 --- a/app/Models/NotificationStream.php +++ b/app/Models/NotificationStream.php @@ -26,6 +26,7 @@ class NotificationStream extends Model public const string TYPE_DISCORD = 'discord_webhook'; public const string TYPE_SLACK = 'slack_webhook'; public const string TYPE_TEAMS = 'teams_webhook'; + public const string TYPE_PUSHOVER = 'pushover'; /** * The attributes that aren't mass assignable. @@ -97,6 +98,14 @@ public function isTeams(): bool return $this->type === self::TYPE_TEAMS; } + /** + * Check if the notification stream type is Pushover. + */ + public function isPushover(): bool + { + return $this->type === self::TYPE_PUSHOVER; + } + /** * Returns whether this stream will send backup notifications on success. */ @@ -127,6 +136,7 @@ protected function formattedType(): Attribute self::TYPE_DISCORD => (string) __('Discord Webhook'), self::TYPE_SLACK => (string) __('Slack Webhook'), self::TYPE_TEAMS => (string) __('Teams Webhook'), + self::TYPE_PUSHOVER => (string) __('Pushover'), default => null, }; } @@ -147,6 +157,7 @@ protected function typeIcon(): Attribute self::TYPE_DISCORD => 'M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z', self::TYPE_SLACK => 'M5.042 15.165a2.528 2.528 0 0 1-2.52 2.523A2.528 2.528 0 0 1 0 15.165a2.527 2.527 0 0 1 2.522-2.52h2.52v2.52zM6.313 15.165a2.527 2.527 0 0 1 2.521-2.52 2.527 2.527 0 0 1 2.521 2.52v6.313A2.528 2.528 0 0 1 8.834 24a2.528 2.528 0 0 1-2.521-2.522v-6.313zM8.834 5.042a2.528 2.528 0 0 1-2.521-2.52A2.528 2.528 0 0 1 8.834 0a2.528 2.528 0 0 1 2.521 2.522v2.52H8.834zM8.834 6.313a2.528 2.528 0 0 1 2.521 2.521 2.528 2.528 0 0 1-2.521 2.521H2.522A2.528 2.528 0 0 1 0 8.834a2.528 2.528 0 0 1 2.522-2.521h6.312zM18.956 8.834a2.528 2.528 0 0 1 2.522-2.521A2.528 2.528 0 0 1 24 8.834a2.528 2.528 0 0 1-2.522 2.521h-2.522V8.834zM17.688 8.834a2.528 2.528 0 0 1-2.523 2.521 2.527 2.527 0 0 1-2.52-2.521V2.522A2.527 2.527 0 0 1 15.165 0a2.528 2.528 0 0 1 2.523 2.522v6.312zM15.165 18.956a2.528 2.528 0 0 1 2.523 2.522A2.528 2.528 0 0 1 15.165 24a2.527 2.527 0 0 1-2.52-2.522v-2.522h2.52zM15.165 17.688a2.527 2.527 0 0 1-2.52-2.523 2.526 2.526 0 0 1 2.52-2.52h6.313A2.527 2.527 0 0 1 24 15.165a2.528 2.528 0 0 1-2.522 2.523h-6.313z', self::TYPE_TEAMS => 'M 12.5 2 A 3 3 0 0 0 9.7089844 6.09375 C 9.4804148 6.0378189 9.2455412 6 9 6 L 4 6 C 2.346 6 1 7.346 1 9 L 1 14 C 1 15.654 2.346 17 4 17 L 9 17 C 10.654 17 12 15.654 12 14 L 12 9 C 12 8.6159715 11.921192 8.2518913 11.789062 7.9140625 A 3 3 0 0 0 12.5 8 A 3 3 0 0 0 12.5 2 z M 19 4 A 2 2 0 0 0 19 8 A 2 2 0 0 0 19 4 z M 4.5 9 L 8.5 9 C 8.776 9 9 9.224 9 9.5 C 9 9.776 8.776 10 8.5 10 L 7 10 L 7 14 C 7 14.276 6.776 14.5 6.5 14.5 C 6.224 14.5 6 14.276 6 14 L 6 10 L 4.5 10 C 4.224 10 4 9.776 4 9.5 C 4 9.224 4.224 9 4.5 9 z M 15 9 C 14.448 9 14 9.448 14 10 L 14 14 C 14 16.761 11.761 19 9 19 C 8.369 19 8.0339375 19.755703 8.4609375 20.220703 C 9.4649375 21.313703 10.903 22 12.5 22 C 15.24 22 17.529453 20.040312 17.939453 17.320312 C 17.979453 17.050312 18 16.78 18 16.5 L 18 11 C 18 9.9 17.1 9 16 9 L 15 9 z M 20.888672 9 C 20.322672 9 19.870625 9.46625 19.890625 10.03125 C 19.963625 12.09325 20 16.5 20 16.5 C 20 16.618 19.974547 16.859438 19.935547 17.148438 C 19.812547 18.048438 20.859594 18.653266 21.558594 18.072266 C 22.439594 17.340266 23 16.237 23 15 L 23 11 C 23 9.9 22.1 9 21 9 L 20.888672 9 z', + self::TYPE_PUSHOVER => 'M11.6685 21.0473c5.2435.1831 9.6426-3.9191 9.8257-9.1627.1831-5.24355-3.9191-9.64267-9.1626-9.82578-5.24355-.18311-9.64265 3.91918-9.82576 9.16268-.18311 5.2435 3.91916 9.6427 9.16266 9.8258zM11.8206 8.47095l1.9374-.1867-2.0265 4.17345c.331-.0144.6576-.1144.9816-.3018.324-.1873.6257-.4274.9014-.7186.2775-.291.5191-.6168.7267-.9791.2075-.3603.3593-.7189.457-1.0701.0577-.2189.0892-.4295.0926-.6317s-.0442-.3822-.1409-.5378c-.0967-.1556-.2463-.2834-.4508-.3833-.2044-.1-.4828-.1561-.8389-.1686-.4153-.0145-.8256.038-1.2309.1594-.4071.1213-.7848.3049-1.1369.5507-.352.2458-.66.5562-.9274.933-.2676.3769-.4646.8082-.5911 1.2939-.0483.1598-.0768.2869-.0895.3849s-.0174.1776-.014.2408c.0015.0632.009.1136.0208.1474.0119.0339.0218.0676.028.1031-.4321-.015-.7443-.1132-.9366-.2926-.1924-.1794-.23-.4852-.1149-.9119.1177-.4452.3625-.8637.7364-1.2572.374-.3936.8129-.73655 1.3188-1.02705.5059-.29055 1.0559-.51825 1.6501-.67945.5942-.1612 1.1704-.2321 1.7285-.2126.4914.0172.9006.102 1.2295.2527.329.1508.5839.3453.7614.5799.1775.2346.2849.5057.3207.8114.0358.3057.0099.6223-.0777.9497-.1066.3936-.2967.7879-.5685 1.18135s-.609.7455-1.0079 1.0565c-.4008.3128-.8551.5605-1.3666.7469-.5096.1846-1.049.2679-1.6164.248l-.0631-.0022-1.7377 3.5597-1.82655-.0638z', default => 'M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z', }; } diff --git a/database/factories/NotificationStreamFactory.php b/database/factories/NotificationStreamFactory.php index 240ac353..7f60e6df 100644 --- a/database/factories/NotificationStreamFactory.php +++ b/database/factories/NotificationStreamFactory.php @@ -20,6 +20,7 @@ public function definition(): array NotificationStream::TYPE_SLACK, NotificationStream::TYPE_DISCORD, NotificationStream::TYPE_TEAMS, + NotificationStream::TYPE_PUSHOVER, ]); return [ @@ -76,6 +77,17 @@ public function teams(): static ]); } + /** + * Indicate that the notification stream is for Pushover. + */ + public function pushover(): static + { + return $this->state([ + 'type' => NotificationStream::TYPE_PUSHOVER, + 'value' => $this->generatePushover(), + ]); + } + /** * Indicate that the notification stream will send successful notifications. */ @@ -166,4 +178,12 @@ private function generateTeamsWebhook(): string return "https://{$subdomain}.webhook.office.com/webhookb2/{$guid1}@{$guid2}/IncomingWebhook/{$alphanumeric}/{$guid3}"; } + + /** + * Generate a realistic Pushover API token. + */ + private function generatePushover(): string + { + return $this->faker->regexify('[a-zA-Z0-9]{30}'); + } } diff --git a/tests/Unit/Models/BackupTaskTest.php b/tests/Unit/Models/BackupTaskTest.php index 0a31b96b..44aac472 100644 --- a/tests/Unit/Models/BackupTaskTest.php +++ b/tests/Unit/Models/BackupTaskTest.php @@ -3,6 +3,7 @@ declare(strict_types=1); use App\Jobs\BackupTasks\SendDiscordNotificationJob; +use App\Jobs\BackupTasks\SendPushoverNotificationJob; use App\Jobs\BackupTasks\SendSlackNotificationJob; use App\Jobs\BackupTasks\SendTeamsNotificationJob; use App\Jobs\RunDatabaseBackupTaskJob; @@ -639,7 +640,24 @@ expect($task->hasTeamNotification())->toBeFalse(); }); -it('queues up a teams notification job if a discord notification has been set', function (): void { +it('returns true if there is a notification stream pushover set', function (): void { + + $task = BackupTask::factory()->create(); + $stream = NotificationStream::factory()->pushover()->create(); + + $task->notificationStreams()->attach($stream); + + expect($task->hasPushoverNotification())->toBeTrue(); +}); + +it('returns false if there is no notification stream pushover set', function (): void { + + $task = BackupTask::factory()->create(); + + expect($task->hasPushoverNotification())->toBeFalse(); +}); + +it('queues up a teams notification job if a teams notification has been set', function (): void { Queue::fake(); @@ -665,6 +683,32 @@ Queue::assertNotPushed(SendTeamsNotificationJob::class); }); +it('queues up a pushover notification job if a pushover notification has been set', function (): void { + + Queue::fake(); + + $task = BackupTask::factory()->create(); + $stream = NotificationStream::factory()->pushover()->create(); + $task->notificationStreams()->attach($stream); + + BackupTaskLog::factory()->create(['backup_task_id' => $task->id]); + + $task->sendNotifications(); + + Queue::assertPushed(SendPushoverNotificationJob::class); +}); + +it('does not queue up a pushover notification job if a pushover notification has not been set', function (): void { + + Queue::fake(); + + $task = BackupTask::factory()->create(); + + $task->sendNotifications(); + + Queue::assertNotPushed(SendPushoverNotificationJob::class); +}); + it('queues up a discord notification job if a discord notification has been set', function (): void { Queue::fake(); @@ -863,6 +907,56 @@ ->toThrow(RuntimeException::class, 'Teams webhook failed: Unauthorized'); }); +it('sends a Pushover notification successfully', function (): void { + $task = BackupTask::factory()->create(); + $log = BackupTaskLog::factory()->create(['backup_task_id' => $task->id]); + $pushoverToken = 'abc123'; + + Http::fake([ + 'https://api.pushover.net/1/messages.json' => Http::response('', 200), + ]); + + $task->sendPushoverNotification($log, $pushoverToken); + + Http::assertSent(fn ($request): bool => $request->url() === 'https://api.pushover.net/1/messages.json'); +}); + +it('sends a Pushover notification for failed backup', function (): void { + $task = BackupTask::factory()->create(); + $log = BackupTaskLog::factory()->create([ + 'backup_task_id' => $task->id, + 'successful_at' => null, + ]); + $pushoverToken = 'abc123'; + + Http::fake([ + 'https://api.pushover.net/1/messages.json' => Http::response('', 200), + ]); + + $task->sendPushoverNotification($log, $pushoverToken); + + Http::assertSent(function (array $request) use ($pushoverToken, $task): bool { + return $request->url() === 'https://api.pushover.net/1/messages.json' + && $request['token'] === $pushoverToken + && $request['title'] === "{$task->label} Backup Task: Failure" + && str_contains((string) $request['message'], 'The backup task failed.') + && $request['priority'] === 1; + }); +}); + +it('throws an exception when Pushover notification fails', function (): void { + $task = BackupTask::factory()->create(); + $log = BackupTaskLog::factory()->create(['backup_task_id' => $task->id]); + $pushoverToken = 'abc123'; + + Http::fake([ + 'https://api.pushover.net/1/messages.json' => Http::response('Error', 500), + ]); + + expect(fn () => $task->sendPushoverNotification($log, $pushoverToken)) + ->toThrow(RuntimeException::class, 'Pushover notification failed: Error'); +}); + it('sends notifications based on backup task outcome and user preferences', function (): void { Queue::fake(); Mail::fake(); diff --git a/tests/Unit/Models/NotificationStreamTest.php b/tests/Unit/Models/NotificationStreamTest.php index e1b512e4..5aeb3646 100644 --- a/tests/Unit/Models/NotificationStreamTest.php +++ b/tests/Unit/Models/NotificationStreamTest.php @@ -44,6 +44,16 @@ expect($notificationStream->isTeams())->toBeFalse(); }); +it('returns true if stream type is pushover', function (): void { + $notificationStream = NotificationStream::factory()->pushover()->create(); + expect($notificationStream->isPushover())->toBeTrue(); +}); + +it('returns false if the stream type is not pushover', function (): void { + $notificationStream = NotificationStream::factory()->email()->create(); + expect($notificationStream->isPushover())->toBeFalse(); +}); + it('returns correct formatted type for email', function (): void { $notificationStream = NotificationStream::factory()->email()->create(); expect($notificationStream->formatted_type)->toBe('Email'); @@ -64,6 +74,11 @@ expect($notificationStream->formatted_type)->toBe('Teams Webhook'); }); +it('returns correct formatted type for pushover', function (): void { + $notificationStream = NotificationStream::factory()->pushover()->create(); + expect($notificationStream->formatted_type)->toBe('Pushover'); +}); + it('returns correct type icon for email', function (): void { $notificationStream = NotificationStream::factory()->email()->create(); expect($notificationStream->type_icon)->toBe('M20 4H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 4l-8 5-8-5V6l8 5 8-5v2z'); @@ -84,6 +99,11 @@ expect($notificationStream->type_icon)->toBe('M 12.5 2 A 3 3 0 0 0 9.7089844 6.09375 C 9.4804148 6.0378189 9.2455412 6 9 6 L 4 6 C 2.346 6 1 7.346 1 9 L 1 14 C 1 15.654 2.346 17 4 17 L 9 17 C 10.654 17 12 15.654 12 14 L 12 9 C 12 8.6159715 11.921192 8.2518913 11.789062 7.9140625 A 3 3 0 0 0 12.5 8 A 3 3 0 0 0 12.5 2 z M 19 4 A 2 2 0 0 0 19 8 A 2 2 0 0 0 19 4 z M 4.5 9 L 8.5 9 C 8.776 9 9 9.224 9 9.5 C 9 9.776 8.776 10 8.5 10 L 7 10 L 7 14 C 7 14.276 6.776 14.5 6.5 14.5 C 6.224 14.5 6 14.276 6 14 L 6 10 L 4.5 10 C 4.224 10 4 9.776 4 9.5 C 4 9.224 4.224 9 4.5 9 z M 15 9 C 14.448 9 14 9.448 14 10 L 14 14 C 14 16.761 11.761 19 9 19 C 8.369 19 8.0339375 19.755703 8.4609375 20.220703 C 9.4649375 21.313703 10.903 22 12.5 22 C 15.24 22 17.529453 20.040312 17.939453 17.320312 C 17.979453 17.050312 18 16.78 18 16.5 L 18 11 C 18 9.9 17.1 9 16 9 L 15 9 z M 20.888672 9 C 20.322672 9 19.870625 9.46625 19.890625 10.03125 C 19.963625 12.09325 20 16.5 20 16.5 C 20 16.618 19.974547 16.859438 19.935547 17.148438 C 19.812547 18.048438 20.859594 18.653266 21.558594 18.072266 C 22.439594 17.340266 23 16.237 23 15 L 23 11 C 23 9.9 22.1 9 21 9 L 20.888672 9 z'); }); +it('returns correct type icon for pushover', function (): void { + $notificationStream = NotificationStream::factory()->pushover()->create(); + expect($notificationStream->type_icon)->toBe('M11.6685 21.0473c5.2435.1831 9.6426-3.9191 9.8257-9.1627.1831-5.24355-3.9191-9.64267-9.1626-9.82578-5.24355-.18311-9.64265 3.91918-9.82576 9.16268-.18311 5.2435 3.91916 9.6427 9.16266 9.8258zM11.8206 8.47095l1.9374-.1867-2.0265 4.17345c.331-.0144.6576-.1144.9816-.3018.324-.1873.6257-.4274.9014-.7186.2775-.291.5191-.6168.7267-.9791.2075-.3603.3593-.7189.457-1.0701.0577-.2189.0892-.4295.0926-.6317s-.0442-.3822-.1409-.5378c-.0967-.1556-.2463-.2834-.4508-.3833-.2044-.1-.4828-.1561-.8389-.1686-.4153-.0145-.8256.038-1.2309.1594-.4071.1213-.7848.3049-1.1369.5507-.352.2458-.66.5562-.9274.933-.2676.3769-.4646.8082-.5911 1.2939-.0483.1598-.0768.2869-.0895.3849s-.0174.1776-.014.2408c.0015.0632.009.1136.0208.1474.0119.0339.0218.0676.028.1031-.4321-.015-.7443-.1132-.9366-.2926-.1924-.1794-.23-.4852-.1149-.9119.1177-.4452.3625-.8637.7364-1.2572.374-.3936.8129-.73655 1.3188-1.02705.5059-.29055 1.0559-.51825 1.6501-.67945.5942-.1612 1.1704-.2321 1.7285-.2126.4914.0172.9006.102 1.2295.2527.329.1508.5839.3453.7614.5799.1775.2346.2849.5057.3207.8114.0358.3057.0099.6223-.0777.9497-.1066.3936-.2967.7879-.5685 1.18135s-.609.7455-1.0079 1.0565c-.4008.3128-.8551.5605-1.3666.7469-.5096.1846-1.049.2679-1.6164.248l-.0631-.0022-1.7377 3.5597-1.82655-.0638z'); +}); + it('returns default type icon for unknown type', function (): void { $notificationStream = NotificationStream::factory()->create(['type' => 'unknown']); expect($notificationStream->type_icon)->toBe('M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm1 15h-2v-6h2v6zm0-8h-2V7h2v2z');