Implements feature 100 (Alert Targets): - US1: “Send test message” action (RBAC + confirmation + rate limit + audit + async job) - US2: Derived “Last test” status badge (Never/Sent/Failed/Pending) on view + edit surfaces - US3: “View last delivery” deep link + deliveries viewer filters (event_type, destination) incl. tenantless test deliveries Tests: - Full suite green (1348 passed, 7 skipped) - Added focused feature tests for send test, last test resolver/badges, and deep-link filters Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #122
116 lines
3.7 KiB
PHP
116 lines
3.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Services\Alerts;
|
|
|
|
use App\Jobs\Alerts\DeliverAlertsJob;
|
|
use App\Models\AlertDelivery;
|
|
use App\Models\AlertDestination;
|
|
use App\Models\User;
|
|
use App\Services\Audit\WorkspaceAuditLogger;
|
|
use App\Support\Audit\AuditActionId;
|
|
use Illuminate\Auth\Access\AuthorizationException;
|
|
|
|
class AlertDestinationTestMessageService
|
|
{
|
|
private const int RATE_LIMIT_SECONDS = 60;
|
|
|
|
public function __construct(
|
|
private WorkspaceAuditLogger $auditLogger,
|
|
) {}
|
|
|
|
/**
|
|
* Send a test message for the given alert destination.
|
|
*
|
|
* @return array{success: bool, message: string, delivery_id: int|null}
|
|
*/
|
|
public function sendTest(AlertDestination $destination, User $actor): array
|
|
{
|
|
if (! $actor->can('update', $destination)) {
|
|
throw new AuthorizationException('You do not have permission to send test messages for this destination.');
|
|
}
|
|
|
|
if (! $destination->is_enabled) {
|
|
return [
|
|
'success' => false,
|
|
'message' => 'This destination is currently disabled. Enable it before sending a test message.',
|
|
'delivery_id' => null,
|
|
];
|
|
}
|
|
|
|
if ($this->isRateLimited($destination)) {
|
|
return [
|
|
'success' => false,
|
|
'message' => 'A test message was sent recently. Please wait before trying again.',
|
|
'delivery_id' => null,
|
|
];
|
|
}
|
|
|
|
$delivery = $this->createTestDelivery($destination);
|
|
|
|
$this->auditLog($destination, $actor);
|
|
|
|
DeliverAlertsJob::dispatch((int) $destination->workspace_id);
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'Test message queued for delivery.',
|
|
'delivery_id' => (int) $delivery->getKey(),
|
|
];
|
|
}
|
|
|
|
private function isRateLimited(AlertDestination $destination): bool
|
|
{
|
|
return AlertDelivery::query()
|
|
->where('workspace_id', (int) $destination->workspace_id)
|
|
->where('alert_destination_id', (int) $destination->getKey())
|
|
->where('event_type', AlertDelivery::EVENT_TYPE_TEST)
|
|
->where('created_at', '>=', now()->subSeconds(self::RATE_LIMIT_SECONDS))
|
|
->exists();
|
|
}
|
|
|
|
private function createTestDelivery(AlertDestination $destination): AlertDelivery
|
|
{
|
|
return AlertDelivery::create([
|
|
'workspace_id' => (int) $destination->workspace_id,
|
|
'tenant_id' => null,
|
|
'alert_rule_id' => null,
|
|
'alert_destination_id' => (int) $destination->getKey(),
|
|
'event_type' => AlertDelivery::EVENT_TYPE_TEST,
|
|
'status' => AlertDelivery::STATUS_QUEUED,
|
|
'severity' => null,
|
|
'fingerprint_hash' => 'test:'.(int) $destination->getKey(),
|
|
'attempt_count' => 0,
|
|
'payload' => [
|
|
'title' => 'Test alert',
|
|
'body' => 'This is a test delivery for destination verification.',
|
|
],
|
|
]);
|
|
}
|
|
|
|
private function auditLog(AlertDestination $destination, User $actor): void
|
|
{
|
|
$workspace = $destination->workspace;
|
|
|
|
if ($workspace === null) {
|
|
return;
|
|
}
|
|
|
|
$this->auditLogger->log(
|
|
workspace: $workspace,
|
|
action: AuditActionId::AlertDestinationTestRequested->value,
|
|
context: [
|
|
'metadata' => [
|
|
'alert_destination_id' => (int) $destination->getKey(),
|
|
'name' => (string) $destination->name,
|
|
'type' => (string) $destination->type,
|
|
],
|
|
],
|
|
actor: $actor,
|
|
resourceType: 'alert_destination',
|
|
resourceId: (string) $destination->getKey(),
|
|
);
|
|
}
|
|
}
|