diff --git a/app/Listeners/CheckoutableListener.php b/app/Listeners/CheckoutableListener.php index 227f6cd278..e647bcd9e0 100644 --- a/app/Listeners/CheckoutableListener.php +++ b/app/Listeners/CheckoutableListener.php @@ -27,6 +27,7 @@ use App\Notifications\CheckoutAssetNotification; use App\Notifications\CheckoutConsumableNotification; use App\Notifications\CheckoutLicenseSeatNotification; use GuzzleHttp\Exception\ClientException; +use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Facades\Mail; use Illuminate\Support\Facades\Notification; use Exception; @@ -40,6 +41,24 @@ class CheckoutableListener Component::class, ]; + /** + * Register the listeners for the subscriber. + * + * @param Illuminate\Events\Dispatcher $events + */ + public function subscribe($events) + { + $events->listen( + \App\Events\CheckoutableCheckedIn::class, + 'App\Listeners\CheckoutableListener@onCheckedIn' + ); + + $events->listen( + \App\Events\CheckoutableCheckedOut::class, + 'App\Listeners\CheckoutableListener@onCheckedOut' + ); + } + /** * Notify the user and post to webhook about the checked out checkoutable * and add a record to the checkout_requests table. @@ -50,94 +69,70 @@ class CheckoutableListener return; } - /** - * Make a checkout acceptance and attach it in the notification - */ - $settings = Setting::getSettings(); + $shouldSendEmailToUser = $this->shouldSendCheckoutEmailToUser($event->checkoutable); + $shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress(); + $shouldSendWebhookNotification = $this->shouldSendWebhookNotification(); + + if (!$shouldSendEmailToUser && !$shouldSendEmailToAlertAddress && !$shouldSendWebhookNotification) { + return; + } + $acceptance = $this->getCheckoutAcceptance($event); - $adminCcEmailsArray = []; - if ($settings->admin_cc_email !== '') { - $adminCcEmail = $settings->admin_cc_email; - $adminCcEmailsArray = array_map('trim', explode(',', $adminCcEmail)); - } - $ccEmails = array_filter($adminCcEmailsArray); - $mailable = $this->getCheckoutMailType($event, $acceptance); - $notifiable = $this->getNotifiableUsers($event); + if ($shouldSendEmailToUser || $shouldSendEmailToAlertAddress) { + $mailable = $this->getCheckoutMailType($event, $acceptance); + $notifiable = $this->getNotifiableUser($event); + $notifiableHasEmail = $notifiable instanceof User && $notifiable->email; - // Send email notifications - try { - /** - * Send an email if any of the following conditions are met: - * 1. The asset requires acceptance - * 2. The item has a EULA - * 3. The item should send an email at check-in/check-out - * 4. If the admin CC email is set, even if the item being checked out doesn't have an email address (location, etc) - */ + $shouldSendEmailToUser = $shouldSendEmailToUser && $notifiableHasEmail; - if ($event->checkoutable->requireAcceptance() /* does category require acceptance? */ || - $event->checkoutable->getEula() /* is there *some* kind of EULA? */ || - $this->checkoutableShouldSendEmail($event) /* does the category have 'checkin_email' [sic] set? */) { + [$to, $cc] = $this->generateEmailRecipients($shouldSendEmailToUser, $shouldSendEmailToAlertAddress, $notifiable); - - // Send a checkout email to the admin CC addresses, even if the target has no email - if (!empty($ccEmails)) { - Mail::to($ccEmails)->send($mailable); - Log::info('Checkout Mail sent to CC addresses'); - } - - // Send a checkout email to the target if it has an email - if (!empty($notifiable->email)) { - Mail::to($notifiable)->send($mailable); + if (!empty($to)) { + try { + Mail::to(array_flatten($to))->cc(array_flatten($cc))->send($mailable); Log::info('Checkout Mail sent to checkout target'); + } catch (ClientException $e) { + Log::debug("Exception caught during checkout email: " . $e->getMessage()); + } catch (Exception $e) { + Log::debug("Exception caught during checkout email: " . $e->getMessage()); } - } - } catch (ClientException $e) { - Log::debug("Exception caught during checkout email: " . $e->getMessage()); - } catch (Exception $e) { - Log::debug("Exception caught during checkout email: " . $e->getMessage()); } - // Send notification - try { - if ($this->shouldSendWebhookNotification()) { + if ($shouldSendWebhookNotification) { + try { if ($this->newMicrosoftTeamsWebhookEnabled()) { $message = $this->getCheckoutNotification($event)->toMicrosoftTeams(); $notification = new TeamsNotification(Setting::getSettings()->webhook_endpoint); $notification->success()->sendMessage($message[0], $message[1]); // Send the message to Microsoft Teams } else { - Notification::route($this->webhookSelected(), Setting::getSettings()->webhook_endpoint) ->notify($this->getCheckoutNotification($event, $acceptance)); } + } catch (ClientException $e) { + if (strpos($e->getMessage(), 'channel_not_found') !== false) { + Log::warning(Setting::getSettings()->webhook_selected . " notification failed: " . $e->getMessage()); + return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_channel_not_found')); + } else { + Log::error("ClientException caught during checkin notification: " . $e->getMessage()); + } + return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_fail')); + } catch (Exception $e) { + Log::warning(ucfirst(Setting::getSettings()->webhook_selected) . ' webhook notification failed:', [ + 'error' => $e->getMessage(), + 'webhook_endpoint' => Setting::getSettings()->webhook_endpoint, + 'event' => $event, + ]); + return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_fail')); } - } catch (ClientException $e) { - if (strpos($e->getMessage(), 'channel_not_found') !== false) { - Log::warning(Setting::getSettings()->webhook_selected." notification failed: " . $e->getMessage()); - return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) .trans('admin/settings/message.webhook.webhook_channel_not_found') ); - } - else { - Log::error("ClientException caught during checkin notification: " . $e->getMessage()); - } - return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) .trans('admin/settings/message.webhook.webhook_fail') ); - } catch (Exception $e) { - Log::warning(ucfirst(Setting::getSettings()->webhook_selected) . ' webhook notification failed:', [ - 'error' => $e->getMessage(), - 'webhook_endpoint' => Setting::getSettings()->webhook_endpoint, - 'event' => $event, - ]); - return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_fail')); } } - - - /** * Notify the user and post to webhook about the checked in checkoutable - */ + */ public function onCheckedIn($event) { Log::debug('onCheckedIn in the Checkoutable listener fired'); @@ -146,49 +141,44 @@ class CheckoutableListener return; } - /** - * Send the appropriate notification - */ - if ($event->checkedOutTo && $event->checkoutable){ - $acceptances = CheckoutAcceptance::where('checkoutable_id', $event->checkoutable->id) - ->where('assigned_to_id', $event->checkedOutTo->id) - ->get(); + $shouldSendEmailToUser = $this->checkoutableCategoryShouldSendEmail($event->checkoutable); + $shouldSendEmailToAlertAddress = $this->shouldSendEmailToAlertAddress(); + $shouldSendWebhookNotification = $this->shouldSendWebhookNotification(); - foreach($acceptances as $acceptance){ - if($acceptance->isPending()){ - $acceptance->delete(); + if (!$shouldSendEmailToUser && !$shouldSendEmailToAlertAddress && !$shouldSendWebhookNotification) { + return; + } + + if ($shouldSendEmailToUser || $shouldSendEmailToAlertAddress) { + /** + * Send the appropriate notification + */ + if ($event->checkedOutTo && $event->checkoutable) { + $acceptances = CheckoutAcceptance::where('checkoutable_id', $event->checkoutable->id) + ->where('assigned_to_id', $event->checkedOutTo->id) + ->get(); + + foreach ($acceptances as $acceptance) { + if ($acceptance->isPending()) { + $acceptance->delete(); + } } } - } - $settings = Setting::getSettings(); - $adminCcEmailsArray = []; - if($settings->admin_cc_email !== '') { - $adminCcEmail = $settings->admin_cc_email; - $adminCcEmailsArray = array_map('trim', explode(',', $adminCcEmail)); - } - $ccEmails = array_filter($adminCcEmailsArray); - $mailable = $this->getCheckinMailType($event); - $notifiable = $this->getNotifiableUsers($event); + $mailable = $this->getCheckinMailType($event); + $notifiable = $this->getNotifiableUser($event); + + $notifiableHasEmail = $notifiable instanceof User && $notifiable->email; + + $shouldSendEmailToUser = $shouldSendEmailToUser && $notifiableHasEmail; + + [$to, $cc] = $this->generateEmailRecipients($shouldSendEmailToUser, $shouldSendEmailToAlertAddress, $notifiable); - // Send email notifications - if ($this->checkoutableShouldSendEmail($event)) { try { - /** - * Send a check-in n email *only* if the item should send an email at check-in/check-out - */ - - // Send a checkout email to the admin's CC addresses, even if the target has no email - if (!empty($ccEmails)) { - Mail::to($ccEmails)->send($mailable); + if (!empty($to)) { + Mail::to(array_flatten($to))->cc(array_flatten($cc))->send($mailable); Log::info('Checkin Mail sent to CC addresses'); } - - // Send a checkout email to the target if it has an email - if (!empty($notifiable->email)) { - Mail::to($notifiable)->send($mailable); - Log::info('Checkin Mail sent to checkout target'); - } } catch (ClientException $e) { Log::debug("Exception caught during checkin email: " . $e->getMessage()); } catch (Exception $e) { @@ -196,9 +186,9 @@ class CheckoutableListener } } - // Send Webhook notification - try { - if ($this->shouldSendWebhookNotification()) { + if ($shouldSendWebhookNotification) { + // Send Webhook notification + try { if ($this->newMicrosoftTeamsWebhookEnabled()) { $message = $this->getCheckinNotification($event)->toMicrosoftTeams(); $notification = new TeamsNotification(Setting::getSettings()->webhook_endpoint); @@ -207,25 +197,24 @@ class CheckoutableListener Notification::route($this->webhookSelected(), Setting::getSettings()->webhook_endpoint) ->notify($this->getCheckinNotification($event)); } - } - } catch (ClientException $e) { - if (strpos($e->getMessage(), 'channel_not_found') !== false) { - Log::warning(Setting::getSettings()->webhook_selected." notification failed: " . $e->getMessage()); - return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) .trans('admin/settings/message.webhook.webhook_channel_not_found') ); - } - else { - Log::error("ClientException caught during checkin notification: " . $e->getMessage()); + } catch (ClientException $e) { + if (strpos($e->getMessage(), 'channel_not_found') !== false) { + Log::warning(Setting::getSettings()->webhook_selected . " notification failed: " . $e->getMessage()); + return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_channel_not_found')); + } else { + Log::error("ClientException caught during checkin notification: " . $e->getMessage()); + return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_fail')); + } + } catch (Exception $e) { + Log::warning(ucfirst(Setting::getSettings()->webhook_selected) . ' webhook notification failed:', [ + 'error' => $e->getMessage(), + 'webhook_endpoint' => Setting::getSettings()->webhook_endpoint, + 'event' => $event, + ]); return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) . trans('admin/settings/message.webhook.webhook_fail')); } - } catch (Exception $e) { - Log::warning(ucfirst(Setting::getSettings()->webhook_selected) . ' webhook notification failed:', [ - 'error' => $e->getMessage(), - 'webhook_endpoint' => Setting::getSettings()->webhook_endpoint, - 'event' => $event, - ]); - return redirect()->back()->with('warning', ucfirst(Setting::getSettings()->webhook_selected) .trans('admin/settings/message.webhook.webhook_fail')); } - } + } /** * Generates a checkout acceptance @@ -247,13 +236,13 @@ class CheckoutableListener $acceptance->assignedTo()->associate($event->checkedOutTo); $acceptance->save(); - return $acceptance; + return $acceptance; } /** * Get the appropriate notification for the event - * - * @param CheckoutableCheckedIn $event + * + * @param CheckoutableCheckedIn $event * @return Notification */ private function getCheckinNotification($event) @@ -267,7 +256,7 @@ class CheckoutableListener break; case Asset::class: $notificationClass = CheckinAssetNotification::class; - break; + break; case LicenseSeat::class: $notificationClass = CheckinLicenseSeatNotification::class; break; @@ -275,9 +264,8 @@ class CheckoutableListener Log::debug('Notification class: '.$notificationClass); - return new $notificationClass($event->checkoutable, $event->checkedOutTo, $event->checkedInBy, $event->note); + return new $notificationClass($event->checkoutable, $event->checkedOutTo, $event->checkedInBy, $event->note); } - /** * Get the appropriate notification for the event * @@ -319,6 +307,7 @@ class CheckoutableListener return new $mailable($event->checkoutable, $event->checkedOutTo, $event->checkedOutBy, $acceptance, $event->note); } + private function getCheckinMailType($event){ $lookup = [ Accessory::class => CheckinAccessoryMail::class, @@ -340,7 +329,8 @@ class CheckoutableListener * @param $event * @return mixed */ - private function getNotifiableUsers($event){ + private function getNotifiableUser($event) + { // If it's assigned to an asset, get that asset's assignedTo object if ($event->checkedOutTo instanceof Asset){ @@ -356,6 +346,7 @@ class CheckoutableListener return $event->checkedOutTo; } } + private function webhookSelected(){ if(Setting::getSettings()->webhook_selected === 'slack' || Setting::getSettings()->webhook_selected === 'general'){ return 'slack'; @@ -364,32 +355,13 @@ class CheckoutableListener return Setting::getSettings()->webhook_selected; } - /** - * Register the listeners for the subscriber. - * - * @param Illuminate\Events\Dispatcher $events - */ - public function subscribe($events) - { - $events->listen( - \App\Events\CheckoutableCheckedIn::class, - 'App\Listeners\CheckoutableListener@onCheckedIn' - ); - - $events->listen( - \App\Events\CheckoutableCheckedOut::class, - 'App\Listeners\CheckoutableListener@onCheckedOut' - ); - } - private function shouldNotSendAnyNotifications($checkoutable): bool { - if(in_array(get_class($checkoutable), $this->skipNotificationsFor)) { - return true; - } - if ($this->shouldSendWebhookNotification()) { - return false; - } + return in_array(get_class($checkoutable), $this->skipNotificationsFor); + } + + private function shouldSendEmailNotifications(Model $checkoutable): bool + { //runs a check if the category wants to send checkin/checkout emails to users $category = match (true) { $checkoutable instanceof Asset => $checkoutable->model->category, @@ -400,27 +372,93 @@ class CheckoutableListener }; if (!$category?->checkin_email) { - return true; + return false; } - return false; + return true; } - private function shouldSendWebhookNotification(): bool { return Setting::getSettings() && Setting::getSettings()->webhook_endpoint; } - private function checkoutableShouldSendEmail($event): bool + private function checkoutableCategoryShouldSendEmail(Model $checkoutable): bool { - if($event->checkoutable instanceof LicenseSeat){ - return $event->checkoutable->license->checkin_email(); + if ($checkoutable instanceof LicenseSeat) { + return $checkoutable->license->checkin_email(); } - return (method_exists($event->checkoutable, 'checkin_email') && $event->checkoutable->checkin_email()); + return (method_exists($checkoutable, 'checkin_email') && $checkoutable->checkin_email()); } private function newMicrosoftTeamsWebhookEnabled(): bool { return Setting::getSettings()->webhook_selected === 'microsoft' && Str::contains(Setting::getSettings()->webhook_endpoint, 'workflows'); } + + private function shouldSendCheckoutEmailToUser(Model $checkoutable): bool + { + /** + * Send an email if any of the following conditions are met: + * 1. The asset requires acceptance + * 2. The item has a EULA + * 3. The item should send an email at check-in/check-out + */ + + if ($checkoutable->requireAcceptance()) { + return true; + } + + if ($checkoutable->getEula()) { + return true; + } + + if ($this->checkoutableCategoryShouldSendEmail($checkoutable)) { + return true; + } + + return false; + } + + private function shouldSendEmailToAlertAddress(): bool + { + return Setting::getSettings() && Setting::getSettings()->admin_cc_email; + } + + private function getFormattedAlertAddresses(): array + { + $alertAddresses = Setting::getSettings()->admin_cc_email; + + if ($alertAddresses !== '') { + return array_filter(array_map('trim', explode(',', $alertAddresses))); + } + + return []; + } + + private function generateEmailRecipients( + bool $shouldSendEmailToUser, + bool $shouldSendEmailToAlertAddress, + mixed $notifiable + ): array { + $to = []; + $cc = []; + + // if user && cc: to user, cc admin + if ($shouldSendEmailToUser && $shouldSendEmailToAlertAddress) { + $to[] = $notifiable; + $cc[] = $this->getFormattedAlertAddresses(); + } + + // if user && no cc: to user + if ($shouldSendEmailToUser && !$shouldSendEmailToAlertAddress) { + $to[] = $notifiable; + } + + // if no user && cc: to admin + if (!$shouldSendEmailToUser && $shouldSendEmailToAlertAddress) { + $to[] = $this->getFormattedAlertAddresses(); + } + + return array($to, $cc); + } } diff --git a/app/Notifications/AuditNotification.php b/app/Notifications/AuditNotification.php index 4f4638e6cd..a7056e8cbd 100644 --- a/app/Notifications/AuditNotification.php +++ b/app/Notifications/AuditNotification.php @@ -2,6 +2,7 @@ namespace App\Notifications; +use AllowDynamicProperties; use App\Models\Setting; use Illuminate\Bus\Queueable; use Illuminate\Notifications\Channels\SlackWebhookChannel; @@ -12,7 +13,7 @@ use Illuminate\Support\Str; use NotificationChannels\MicrosoftTeams\MicrosoftTeamsChannel; use NotificationChannels\MicrosoftTeams\MicrosoftTeamsMessage; -class AuditNotification extends Notification +#[AllowDynamicProperties] class AuditNotification extends Notification { use Queueable; /** diff --git a/database/factories/CategoryFactory.php b/database/factories/CategoryFactory.php index 449af0caa5..733c52668e 100644 --- a/database/factories/CategoryFactory.php +++ b/database/factories/CategoryFactory.php @@ -28,7 +28,7 @@ class CategoryFactory extends Factory 'checkin_email' => true, 'eula_text' => $this->faker->paragraph(), 'require_acceptance' => false, - 'use_default_eula' => $this->faker->boolean(), + 'use_default_eula' => false, 'created_by' => User::factory()->superuser(), 'notes' => 'Created by DB seeder', ]; diff --git a/resources/views/groups/view.blade.php b/resources/views/groups/view.blade.php index 421865ac94..edb6279974 100644 --- a/resources/views/groups/view.blade.php +++ b/resources/views/groups/view.blade.php @@ -22,7 +22,7 @@
-
+
-
+
diff --git a/tests/Feature/Notifications/Email/EmailNotificationsUponCheckinTest.php b/tests/Feature/Notifications/Email/EmailNotificationsUponCheckinTest.php index 9579f4c2e3..254e2e09ca 100644 --- a/tests/Feature/Notifications/Email/EmailNotificationsUponCheckinTest.php +++ b/tests/Feature/Notifications/Email/EmailNotificationsUponCheckinTest.php @@ -4,6 +4,8 @@ namespace Tests\Feature\Notifications\Email; use App\Mail\CheckinAssetMail; use App\Models\Accessory; +use App\Models\AssetModel; +use App\Models\Category; use App\Models\Consumable; use App\Models\LicenseSeat; use Illuminate\Support\Facades\Mail; @@ -11,8 +13,6 @@ use PHPUnit\Framework\Attributes\Group; use App\Events\CheckoutableCheckedIn; use App\Models\Asset; use App\Models\User; -use App\Notifications\CheckinAssetNotification; -use Illuminate\Support\Facades\Notification; use Tests\TestCase; #[Group('notifications')] @@ -25,10 +25,8 @@ class EmailNotificationsUponCheckinTest extends TestCase Mail::fake(); } - public function testCheckInEmailSentToUserIfSettingEnabled() + public function test_check_in_email_sent_to_user_if_setting_enabled() { - Mail::fake(); - $user = User::factory()->create(); $asset = Asset::factory()->assignedToUser($user)->create(); @@ -39,13 +37,11 @@ class EmailNotificationsUponCheckinTest extends TestCase Mail::assertSent(CheckinAssetMail::class, function($mail) use ($user) { return $mail->hasTo($user->email); }); - } - public function testCheckInEmailNotSentToUserIfSettingDisabled() + public function test_check_in_email_not_sent_to_user_if_setting_disabled() { $this->settings->disableAdminCC(); - Mail::fake(); $user = User::factory()->create(); $checkoutables = collect([ @@ -93,6 +89,69 @@ class EmailNotificationsUponCheckinTest extends TestCase }); } + public function test_handles_user_not_having_email_address_set() + { + $user = User::factory()->create(['email' => null]); + $asset = Asset::factory()->assignedToUser($user)->create(); + + $asset->model->category->update(['checkin_email' => true]); + + $this->fireCheckInEvent($asset, $user); + + Mail::assertNothingSent(); + } + + public function test_admin_alert_email_sends() + { + $this->settings->enableAdminCC('cc@example.com'); + + $user = User::factory()->create(); + $asset = Asset::factory()->assignedToUser($user)->create(); + + $asset->model->category->update(['checkin_email' => true]); + + $this->fireCheckInEvent($asset, $user); + + Mail::assertSent(CheckinAssetMail::class, function ($mail) use ($user) { + return $mail->hasTo($user->email) && $mail->hasCc('cc@example.com'); + }); + } + + public function test_admin_alert_email_still_sent_when_category_email_is_not_set_to_send_email_to_user() + { + $this->settings->enableAdminCC('cc@example.com'); + + $category = Category::factory()->create([ + 'checkin_email' => false, + 'eula_text' => null, + 'use_default_eula' => false, + ]); + $assetModel = AssetModel::factory()->create(['category_id' => $category->id]); + $asset = Asset::factory()->create(['model_id' => $assetModel->id]); + + $this->fireCheckInEvent($asset, User::factory()->create()); + + Mail::assertSent(CheckinAssetMail::class, function ($mail) { + return $mail->hasTo('cc@example.com'); + }); + } + + public function test_admin_alert_email_still_sent_when_user_has_no_email_address() + { + $this->settings->enableAdminCC('cc@example.com'); + + $user = User::factory()->create(['email' => null]); + $asset = Asset::factory()->assignedToUser($user)->create(); + + $asset->model->category->update(['checkin_email' => true]); + + $this->fireCheckInEvent($asset, $user); + + Mail::assertSent(CheckinAssetMail::class, function ($mail) { + return $mail->hasTo('cc@example.com'); + }); + } + private function fireCheckInEvent($asset, $user): void { event(new CheckoutableCheckedIn( diff --git a/tests/Feature/Notifications/Email/EmailNotificationsUponCheckoutTest.php b/tests/Feature/Notifications/Email/EmailNotificationsUponCheckoutTest.php new file mode 100644 index 0000000000..e5a1a66cb6 --- /dev/null +++ b/tests/Feature/Notifications/Email/EmailNotificationsUponCheckoutTest.php @@ -0,0 +1,144 @@ +category = Category::factory()->create([ + 'checkin_email' => false, + 'eula_text' => null, + 'require_acceptance' => false, + 'use_default_eula' => false, + ]); + + $this->assetModel = AssetModel::factory()->for($this->category)->create(); + $this->asset = Asset::factory()->for($this->assetModel, 'model')->create(); + + $this->user = User::factory()->create(); + } + + public function test_email_sent_to_user_when_category_requires_acceptance() + { + $this->category->update(['require_acceptance' => true]); + + $this->fireCheckoutEvent(); + + $this->assertUserSentEmail(); + } + + public function test_email_sent_to_user_when_category_using_default_eula() + { + $this->settings->setEula(); + + $this->category->update(['use_default_eula' => true]); + + $this->fireCheckoutEvent(); + + $this->assertUserSentEmail(); + } + + public function test_email_sent_to_user_when_category_using_local_eula() + { + $this->category->update(['eula_text' => 'Some EULA text']); + + $this->fireCheckoutEvent(); + + $this->assertUserSentEmail(); + } + + public function test_email_sent_to_user_when_category_set_to_explicitly_send_email() + { + $this->category->update(['checkin_email' => true]); + + $this->fireCheckoutEvent(); + + $this->assertUserSentEmail(); + } + + public function test_handles_user_not_having_email_address_set() + { + $this->category->update(['checkin_email' => true]); + $this->user->update(['email' => null]); + + $this->fireCheckoutEvent(); + + Mail::assertNothingSent(); + } + + public function test_admin_alert_email_sends() + { + $this->settings->enableAdminCC('cc@example.com'); + + $this->category->update(['checkin_email' => true]); + + $this->fireCheckoutEvent(); + + Mail::assertSent(CheckoutAssetMail::class, function (CheckoutAssetMail $mail) { + return $mail->hasCc('cc@example.com'); + }); + } + + public function test_admin_alert_email_still_sent_when_category_is_not_set_to_send_email_to_user() + { + $this->settings->enableAdminCC('cc@example.com'); + + $this->fireCheckoutEvent(); + + Mail::assertSent(CheckoutAssetMail::class, function ($mail) { + return $mail->hasTo('cc@example.com'); + }); + } + + public function test_admin_alert_email_still_sent_when_user_has_no_email_address() + { + $this->settings->enableAdminCC('cc@example.com'); + + $this->category->update(['checkin_email' => true]); + $this->user->update(['email' => null]); + + $this->fireCheckoutEvent(); + + Mail::assertSent(CheckoutAssetMail::class, function ($mail) { + return $mail->hasTo('cc@example.com'); + }); + } + + private function fireCheckoutEvent(): void + { + event(new CheckoutableCheckedOut( + $this->asset, + $this->user, + User::factory()->superuser()->create(), + '', + )); + } + + private function assertUserSentEmail(): void + { + Mail::assertSent(CheckoutAssetMail::class, function (CheckoutAssetMail $mail) { + return $mail->hasTo($this->user->email); + }); + } +} diff --git a/tests/Feature/Notifications/Webhooks/SlackNotificationsUponCheckinTest.php b/tests/Feature/Notifications/Webhooks/SlackNotificationsUponCheckinTest.php index adaede07ec..e2a97c1603 100644 --- a/tests/Feature/Notifications/Webhooks/SlackNotificationsUponCheckinTest.php +++ b/tests/Feature/Notifications/Webhooks/SlackNotificationsUponCheckinTest.php @@ -2,6 +2,8 @@ namespace Tests\Feature\Notifications\Webhooks; +use App\Models\AssetModel; +use App\Models\Category; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use App\Events\CheckoutableCheckedIn; @@ -95,6 +97,27 @@ class SlackNotificationsUponCheckinTest extends TestCase $this->assertNoSlackNotificationSent(CheckinAssetNotification::class); } + public function testSlackNotificationIsStillSentWhenCategoryEmailIsNotSetToSendEmails() + { + $this->settings->enableSlackWebhook(); + + $category = Category::factory()->create([ + 'checkin_email' => false, + 'eula_text' => null, + 'require_acceptance' => false, + 'use_default_eula' => false, + ]); + $assetModel = AssetModel::factory()->for($category)->create(); + $asset = Asset::factory()->for($assetModel, 'model')->assignedToUser()->create(); + + $this->fireCheckInEvent( + $asset, + User::factory()->create(), + ); + + $this->assertSlackNotificationSent(CheckinAssetNotification::class); + } + public function testComponentCheckinDoesNotSendSlackNotification() { $this->settings->enableSlackWebhook(); diff --git a/tests/Feature/Notifications/Webhooks/SlackNotificationsUponCheckoutTest.php b/tests/Feature/Notifications/Webhooks/SlackNotificationsUponCheckoutTest.php index 4f34da2857..c9f426dd30 100644 --- a/tests/Feature/Notifications/Webhooks/SlackNotificationsUponCheckoutTest.php +++ b/tests/Feature/Notifications/Webhooks/SlackNotificationsUponCheckoutTest.php @@ -2,6 +2,8 @@ namespace Tests\Feature\Notifications\Webhooks; +use App\Models\AssetModel; +use App\Models\Category; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; use App\Events\CheckoutableCheckedOut; @@ -97,6 +99,27 @@ class SlackNotificationsUponCheckoutTest extends TestCase $this->assertNoSlackNotificationSent(CheckoutAssetNotification::class); } + public function testSlackNotificationIsStillSentWhenCategoryEmailIsNotSetToSendEmails() + { + $this->settings->enableSlackWebhook(); + + $category = Category::factory()->create([ + 'checkin_email' => false, + 'eula_text' => null, + 'require_acceptance' => false, + 'use_default_eula' => false, + ]); + $assetModel = AssetModel::factory()->for($category)->create(); + $asset = Asset::factory()->for($assetModel, 'model')->create(); + + $this->fireCheckOutEvent( + $asset, + User::factory()->create(), + ); + + $this->assertSlackNotificationSent(CheckoutAssetNotification::class); + } + public function testComponentCheckoutDoesNotSendSlackNotification() { $this->settings->enableSlackWebhook(); diff --git a/tests/Support/Settings.php b/tests/Support/Settings.php index a4c6e65ccc..bcbd83d8b6 100644 --- a/tests/Support/Settings.php +++ b/tests/Support/Settings.php @@ -46,6 +46,13 @@ class Settings ]); } + public function enableAdminCC(string $email = 'cc@example.co'): Settings + { + return $this->update([ + 'admin_cc_email' => $email, + ]); + } + public function disableAdminCC(): Settings { return $this->update([