create(); $otherProject = Project::factory()->create(); $storeAction->handle($project, [ 'name' => 'Discord alerts', 'enabled' => false, 'event_type' => NotificationEventType::TicketStatusChanged, 'channel' => NotificationChannel::Discord, 'channel_configuration' => ['mentions' => ['ops']], 'message_template' => '$ticket_key changed to $status_to', 'delivery_url' => 'https://discord.com/api/webhooks/123456/discord-alerts', ]); $webhookAction = $storeAction->handle($project, [ 'name' => 'Webhook alerts', 'enabled' => true, 'event_type' => NotificationEventType::TicketStatusChanged, 'channel' => NotificationChannel::Webhook, 'channel_configuration' => ['source' => 'n8n'], 'message_template' => null, 'delivery_url' => 'https://hooks.example.test/kanbangit/webhook-alerts', ]); NotificationAction::factory()->for($otherProject)->create([ 'name' => 'Foreign project webhook', ]); $listedActions = $listAction->handle($project); $this->assertModelExists($webhookAction); $this->assertCount(2, $listedActions); $this->assertSame(['Discord alerts', 'Webhook alerts'], $listedActions->pluck('name')->all()); $this->assertSame(['mentions' => ['ops']], $listedActions->first()->channel_configuration); } /** * Ensure notification actions can be updated without overwriting their saved secret URL. */ public function test_notification_actions_can_be_updated_without_replacing_the_existing_delivery_url(): void { $updateAction = app(UpdateProjectNotificationAction::class); $project = Project::factory()->create(); $notificationAction = NotificationAction::factory()->discord()->for($project)->create([ 'delivery_url' => 'https://discord.com/api/webhooks/123456/original-secret', 'message_template' => '$ticket_key moved to $status_to', ]); $updatedAction = $updateAction->handle($project, $notificationAction, [ 'enabled' => false, 'message_template' => '$ticket_key moved from $status_from to $status_to', 'channel_configuration' => ['mentions' => ['support']], 'delivery_url' => '', ]); $this->assertFalse($updatedAction->enabled); $this->assertSame( '$ticket_key moved from $status_from to $status_to', $updatedAction->message_template, ); $this->assertSame(['mentions' => ['support']], $updatedAction->channel_configuration); $this->assertSame( 'https://discord.com/api/webhooks/123456/original-secret', $updatedAction->delivery_url, ); } /** * Ensure notification actions can replace their delivery URL when a new secret is submitted. */ public function test_notification_actions_can_replace_delivery_urls(): void { $updateAction = app(UpdateProjectNotificationAction::class); $project = Project::factory()->create(); $notificationAction = NotificationAction::factory()->for($project)->create([ 'delivery_url' => 'https://hooks.example.test/kanbangit/original-secret', ]); $updatedAction = $updateAction->handle($project, $notificationAction, [ 'delivery_url' => 'https://hooks.example.test/kanbangit/replaced-secret', 'name' => 'Primary webhook', ]); $this->assertSame('Primary webhook', $updatedAction->name); $this->assertSame('https://hooks.example.test/kanbangit/replaced-secret', $updatedAction->delivery_url); } /** * Ensure notification actions can be deleted from their owning project. */ public function test_notification_actions_can_be_deleted_from_their_project(): void { $deleteAction = app(DeleteProjectNotificationAction::class); $project = Project::factory()->create(); $notificationAction = NotificationAction::factory()->for($project)->create(); $deleteAction->handle($project, $notificationAction); $this->assertModelMissing($notificationAction); } /** * Ensure project-scoped actions reject notification actions from other projects. */ public function test_project_scoped_management_rejects_foreign_notification_actions(): void { $updateAction = app(UpdateProjectNotificationAction::class); $project = Project::factory()->create(); $otherProject = Project::factory()->create(); $notificationAction = NotificationAction::factory()->for($otherProject)->create(); $this->expectException(ModelNotFoundException::class); $updateAction->handle($project, $notificationAction, [ 'name' => 'Should not update', ]); } }