create([ 'name' => 'Erica Example', ]); $project = Project::factory()->create([ 'key' => 'CORE', 'name' => 'Core Platform', ]); $otherProject = Project::factory()->create(); app(InitializeProjectTicketMetadata::class)->handle($project); app(InitializeProjectTicketMetadata::class)->handle($otherProject); $backlogStatus = $this->backlogStatus($project); $ticketType = $project->ticketTypes() ->where('is_parent', false) ->orderBy('position') ->firstOrFail(); $priority = $project->ticketPriorities()->firstOrFail(); $webhookAction = NotificationAction::factory() ->webhook() ->for($project) ->create([ 'name' => 'Primary webhook', 'event_type' => NotificationEventType::TicketCreate, ]); $discordAction = NotificationAction::factory() ->discord() ->for($project) ->create([ 'name' => 'Discord alerts', 'event_type' => NotificationEventType::TicketCreate, ]); $disabledAction = NotificationAction::factory() ->disabled() ->for($project) ->create([ 'name' => 'Disabled webhook', 'event_type' => NotificationEventType::TicketCreate, ]); $statusChangedAction = NotificationAction::factory() ->for($project) ->create([ 'name' => 'Status changed webhook', 'event_type' => NotificationEventType::TicketStatusChanged, ]); $filterNotExistsAction = NotificationAction::factory() ->webhook() ->for($project) ->create([ 'name' => 'Ticket create without origin status', 'event_type' => NotificationEventType::TicketCreate, 'filters' => [ [ 'attribute' => 'status.from.id', 'operator' => 'not_exists', 'value' => null, ], ], ]); $filterExistsAction = NotificationAction::factory() ->webhook() ->for($project) ->create([ 'name' => 'Ticket create with origin status', 'event_type' => NotificationEventType::TicketCreate, 'filters' => [ [ 'attribute' => 'status.from.id', 'operator' => 'exists', 'value' => null, ], ], ]); $foreignProjectAction = NotificationAction::factory() ->for($otherProject) ->create([ 'name' => 'Foreign webhook', 'event_type' => NotificationEventType::TicketCreate, ]); $this->syncProjectPermissions($project, $user, ['edit', 'view']); $response = $this->actingAs($user) ->post(route('projects.tickets.store', $project), [ 'acceptance_criteria' => 'Ship it.', 'assignee_user_id' => null, 'description' => 'Created from notification dispatch test.', 'story_points' => 3, 'ticket_priority_id' => $priority->id, 'ticket_status_id' => $backlogStatus->id, 'ticket_type_id' => $ticketType->id, 'title' => 'Trigger ticket created notification', ]); $ticket = $project->tickets() ->where('ticket_key', 'CORE-1') ->firstOrFail(); $response ->assertRedirect(route('projects.tickets.show', [$project, $ticket])) ->assertSessionHas('status', 'CORE-1 was created.'); Queue::assertPushed(DeliverNotificationAction::class, 3); Queue::assertPushed(DeliverNotificationAction::class, function (DeliverNotificationAction $job) use ($webhookAction, $project, $ticket, $user, $backlogStatus): bool { return $job->notificationActionId === $webhookAction->id && $job->event->payload()->projectId === $project->id && $job->event->payload()->projectKey === 'CORE' && $job->event->payload()->projectName === 'Core Platform' && $job->event->payload()->ticketId === $ticket->id && $job->event->payload()->ticketKey === $ticket->ticket_key && $job->event->payload()->ticketTitle === 'Trigger ticket created notification' && $job->event->payload()->ticketUrl === route('projects.tickets.show', [$project, $ticket]) && $job->event->payload()->actorId === $user->id && $job->event->payload()->actorName === 'Erica Example' && $job->event->payload()->fromStatusId === null && $job->event->payload()->fromStatusLabel === null && $job->event->payload()->toStatusId === $backlogStatus->id && $job->event->payload()->toStatusLabel === 'Backlog' && $job->event->eventType() === NotificationEventType::TicketCreate && $job->notificationDeliveryLogId !== null; }); Queue::assertPushed(DeliverNotificationAction::class, fn (DeliverNotificationAction $job): bool => $job->notificationActionId === $discordAction->id && $job->event->eventType() === NotificationEventType::TicketCreate && $job->notificationDeliveryLogId !== null); Queue::assertPushed(DeliverNotificationAction::class, fn (DeliverNotificationAction $job): bool => $job->notificationActionId === $filterNotExistsAction->id && $job->event->eventType() === NotificationEventType::TicketCreate && $job->notificationDeliveryLogId !== null); Queue::assertNotPushed(DeliverNotificationAction::class, fn (DeliverNotificationAction $job): bool => $job->notificationActionId === $disabledAction->id); Queue::assertNotPushed(DeliverNotificationAction::class, fn (DeliverNotificationAction $job): bool => $job->notificationActionId === $statusChangedAction->id); Queue::assertNotPushed(DeliverNotificationAction::class, fn (DeliverNotificationAction $job): bool => $job->notificationActionId === $filterExistsAction->id); Queue::assertNotPushed(DeliverNotificationAction::class, fn (DeliverNotificationAction $job): bool => $job->notificationActionId === $foreignProjectAction->id); $this->assertDatabaseCount('notification_delivery_logs', 3); $this->assertDatabaseHas('notification_delivery_logs', [ 'notification_action_id' => $webhookAction->id, 'delivery_status' => NotificationDeliveryStatus::Pending->value, 'event_type' => NotificationEventType::TicketCreate->value, 'attempt_count' => 0, 'http_status' => null, ]); $this->assertDatabaseHas('notification_delivery_logs', [ 'notification_action_id' => $discordAction->id, 'delivery_status' => NotificationDeliveryStatus::Pending->value, 'event_type' => NotificationEventType::TicketCreate->value, 'attempt_count' => 0, 'http_status' => null, ]); $this->assertDatabaseHas('notification_delivery_logs', [ 'notification_action_id' => $filterNotExistsAction->id, 'delivery_status' => NotificationDeliveryStatus::Pending->value, 'event_type' => NotificationEventType::TicketCreate->value, 'attempt_count' => 0, 'http_status' => null, ]); } private function backlogStatus(Project $project): TicketStatus { return $project->ticketStatuses() ->whereNull('board_column_id') ->firstOrFail(); } }