test: add comprehensive tests for EntitlementWebhookService

Adds 48 tests (113 assertions) covering webhook registration, dispatch,
signature verification, circuit breaker behaviour, retry logic,
SSRF protection, and workspace queries. Fixes #16.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Claude 2026-03-24 13:41:47 +00:00
parent c51e4310b1
commit beb17eb621
No known key found for this signature in database
GPG key ID: AF404715446AEB41

View file

@ -0,0 +1,896 @@
<?php
declare(strict_types=1);
namespace Core\Tenant\Tests\Feature;
use Core\Tenant\Enums\WebhookDeliveryStatus;
use Core\Tenant\Events\Webhook\LimitWarningEvent;
use Core\Tenant\Exceptions\InvalidWebhookUrlException;
use Core\Tenant\Jobs\DispatchEntitlementWebhook;
use Core\Tenant\Models\EntitlementWebhook;
use Core\Tenant\Models\EntitlementWebhookDelivery;
use Core\Tenant\Models\Feature;
use Core\Tenant\Models\Workspace;
use Core\Tenant\Services\EntitlementWebhookService;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Support\Facades\Bus;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Http;
use Illuminate\Support\Str;
use Tests\TestCase;
class EntitlementWebhookServiceTest extends TestCase
{
use RefreshDatabase;
protected EntitlementWebhookService $service;
protected Workspace $workspace;
protected function setUp(): void
{
parent::setUp();
Cache::flush();
$this->workspace = Workspace::create([
'name' => 'Test Workspace',
'slug' => 'test-workspace',
'domain' => 'test.host.uk.com',
'type' => 'cms',
'is_active' => true,
'sort_order' => 1,
]);
$this->service = app(EntitlementWebhookService::class);
}
// -------------------------------------------------------------------------
// Helper
// -------------------------------------------------------------------------
protected function createWebhook(array $overrides = []): EntitlementWebhook
{
return EntitlementWebhook::create(array_merge([
'workspace_id' => $this->workspace->id,
'name' => 'Test Webhook',
'url' => 'https://example.com/webhook',
'secret' => 'test-secret',
'events' => ['limit_warning'],
'is_active' => true,
'max_attempts' => 3,
], $overrides));
}
protected function createFeature(): Feature
{
return Feature::create([
'code' => 'test.feature.'.Str::random(6),
'name' => 'Test Feature',
'type' => Feature::TYPE_LIMIT,
'reset_type' => Feature::RESET_MONTHLY,
'is_active' => true,
'sort_order' => 1,
]);
}
protected function createDelivery(EntitlementWebhook $webhook, array $overrides = []): EntitlementWebhookDelivery
{
return $webhook->deliveries()->create(array_merge([
'uuid' => Str::uuid(),
'event' => 'limit_warning',
'attempts' => 1,
'status' => WebhookDeliveryStatus::FAILED,
'http_status' => 500,
'payload' => ['event' => 'limit_warning', 'data' => ['test' => true]],
'response' => ['error' => 'Server Error'],
'created_at' => now(),
], $overrides));
}
// -------------------------------------------------------------------------
// Webhook Registration
// -------------------------------------------------------------------------
public function test_it_registers_a_webhook_with_valid_https_url(): void
{
$webhook = $this->service->register(
workspace: $this->workspace,
name: 'Test Webhook',
url: 'https://example.com/webhook',
events: ['limit_warning', 'limit_reached'],
);
$this->assertInstanceOf(EntitlementWebhook::class, $webhook);
$this->assertSame('Test Webhook', $webhook->name);
$this->assertSame('https://example.com/webhook', $webhook->url);
$this->assertSame(['limit_warning', 'limit_reached'], $webhook->events);
$this->assertTrue($webhook->is_active);
$this->assertSame(3, $webhook->max_attempts);
$this->assertSame($this->workspace->id, $webhook->workspace_id);
$this->assertDatabaseHas('entitlement_webhooks', [
'workspace_id' => $this->workspace->id,
'name' => 'Test Webhook',
'url' => 'https://example.com/webhook',
'is_active' => true,
]);
}
public function test_it_generates_a_secret_when_none_is_provided(): void
{
$webhook = $this->service->register(
workspace: $this->workspace,
name: 'Auto Secret Webhook',
url: 'https://example.com/webhook',
events: ['limit_warning'],
);
// Secret should be a 64-character hex string (32 bytes)
$this->assertNotNull($webhook->secret);
$this->assertSame(64, strlen($webhook->secret));
}
public function test_it_uses_the_provided_secret_when_given(): void
{
$secret = 'my-custom-secret-key';
$webhook = $this->service->register(
workspace: $this->workspace,
name: 'Custom Secret Webhook',
url: 'https://example.com/webhook',
events: ['limit_warning'],
secret: $secret,
);
$this->assertSame($secret, $webhook->secret);
}
public function test_it_filters_events_to_only_allowed_values_during_registration(): void
{
$webhook = $this->service->register(
workspace: $this->workspace,
name: 'Filtered Events Webhook',
url: 'https://example.com/webhook',
events: ['limit_warning', 'invalid_event', 'boost_activated', 'nonexistent'],
);
$this->assertEquals(['limit_warning', 'boost_activated'], array_values($webhook->events));
}
public function test_it_stores_metadata_during_registration(): void
{
$metadata = ['source' => 'api', 'version' => '1.0'];
$webhook = $this->service->register(
workspace: $this->workspace,
name: 'Metadata Webhook',
url: 'https://example.com/webhook',
events: ['limit_warning'],
metadata: $metadata,
);
$this->assertSame($metadata, $webhook->metadata);
}
// -------------------------------------------------------------------------
// Webhook Unregistration
// -------------------------------------------------------------------------
public function test_it_unregisters_a_webhook(): void
{
$webhook = $this->service->register(
workspace: $this->workspace,
name: 'To Delete',
url: 'https://example.com/webhook',
events: ['limit_warning'],
);
$result = $this->service->unregister($webhook);
$this->assertTrue($result);
$this->assertDatabaseMissing('entitlement_webhooks', ['id' => $webhook->id]);
}
// -------------------------------------------------------------------------
// Webhook Updates
// -------------------------------------------------------------------------
public function test_it_updates_webhook_attributes(): void
{
$webhook = $this->service->register(
workspace: $this->workspace,
name: 'Original Name',
url: 'https://example.com/webhook',
events: ['limit_warning'],
);
$updated = $this->service->update($webhook, [
'name' => 'Updated Name',
'events' => ['limit_reached', 'boost_activated'],
]);
$this->assertSame('Updated Name', $updated->name);
$this->assertSame(['limit_reached', 'boost_activated'], $updated->events);
}
public function test_it_validates_the_url_when_updating_to_a_new_url(): void
{
$webhook = $this->service->register(
workspace: $this->workspace,
name: 'URL Change Test',
url: 'https://example.com/webhook',
events: ['limit_warning'],
);
$this->expectException(InvalidWebhookUrlException::class);
$this->service->update($webhook, [
'url' => 'http://localhost/evil',
]);
}
public function test_it_skips_ssrf_validation_when_url_has_not_changed(): void
{
$webhook = $this->service->register(
workspace: $this->workspace,
name: 'Same URL Test',
url: 'https://example.com/webhook',
events: ['limit_warning'],
);
$updated = $this->service->update($webhook, [
'name' => 'Renamed',
'url' => 'https://example.com/webhook',
]);
$this->assertSame('Renamed', $updated->name);
$this->assertSame('https://example.com/webhook', $updated->url);
}
public function test_it_filters_events_to_only_allowed_values_during_update(): void
{
$webhook = $this->service->register(
workspace: $this->workspace,
name: 'Event Filter Test',
url: 'https://example.com/webhook',
events: ['limit_warning'],
);
$updated = $this->service->update($webhook, [
'events' => ['boost_expired', 'fake_event', 'package_changed'],
]);
$this->assertEquals(['boost_expired', 'package_changed'], array_values($updated->events));
}
// -------------------------------------------------------------------------
// Signature Verification
// -------------------------------------------------------------------------
public function test_it_signs_a_payload_with_hmac_sha256(): void
{
$payload = ['event' => 'test', 'data' => ['key' => 'value']];
$secret = 'test-secret';
$signature = $this->service->sign($payload, $secret);
$expected = hash_hmac('sha256', json_encode($payload), $secret);
$this->assertSame($expected, $signature);
}
public function test_it_verifies_a_valid_signature(): void
{
$payload = ['event' => 'test', 'data' => ['key' => 'value']];
$secret = 'test-secret';
$signature = $this->service->sign($payload, $secret);
$result = $this->service->verifySignature($payload, $signature, $secret);
$this->assertTrue($result);
}
public function test_it_rejects_an_invalid_signature(): void
{
$payload = ['event' => 'test', 'data' => ['key' => 'value']];
$secret = 'test-secret';
$result = $this->service->verifySignature($payload, 'invalid-signature', $secret);
$this->assertFalse($result);
}
public function test_it_rejects_a_signature_computed_with_a_different_secret(): void
{
$payload = ['event' => 'test', 'data' => ['key' => 'value']];
$signature = $this->service->sign($payload, 'secret-a');
$result = $this->service->verifySignature($payload, $signature, 'secret-b');
$this->assertFalse($result);
}
public function test_it_rejects_a_signature_computed_from_a_different_payload(): void
{
$secret = 'test-secret';
$signature = $this->service->sign(['event' => 'original'], $secret);
$result = $this->service->verifySignature(['event' => 'tampered'], $signature, $secret);
$this->assertFalse($result);
}
// -------------------------------------------------------------------------
// Dispatch (async)
// -------------------------------------------------------------------------
public function test_it_dispatches_webhooks_asynchronously_via_job_queue(): void
{
Bus::fake([DispatchEntitlementWebhook::class]);
$feature = $this->createFeature();
$webhook = $this->createWebhook();
$event = new LimitWarningEvent(
workspace: $this->workspace,
feature: $feature,
used: 80,
limit: 100,
);
$results = $this->service->dispatch($this->workspace, $event, async: true);
$this->assertCount(1, $results);
$this->assertSame($webhook->id, $results[0]['webhook_id']);
$this->assertTrue($results[0]['success']);
$this->assertTrue($results[0]['queued']);
Bus::assertDispatched(DispatchEntitlementWebhook::class);
}
public function test_it_dispatches_webhooks_synchronously_when_async_is_false(): void
{
Http::fake([
'https://example.com/webhook' => Http::response(['ok' => true], 200),
]);
$feature = $this->createFeature();
$webhook = $this->createWebhook();
$event = new LimitWarningEvent(
workspace: $this->workspace,
feature: $feature,
used: 80,
limit: 100,
);
$results = $this->service->dispatch($this->workspace, $event, async: false);
$this->assertCount(1, $results);
$this->assertSame($webhook->id, $results[0]['webhook_id']);
$this->assertTrue($results[0]['success']);
$this->assertNotNull($results[0]['delivery_id']);
}
public function test_it_only_dispatches_to_active_webhooks_subscribed_to_the_event(): void
{
Bus::fake([DispatchEntitlementWebhook::class]);
$feature = $this->createFeature();
// Active and subscribed
$this->createWebhook(['name' => 'Active Subscribed', 'url' => 'https://example.com/webhook-1']);
// Active but not subscribed to this event
$this->createWebhook([
'name' => 'Active Unsubscribed',
'url' => 'https://example.com/webhook-2',
'events' => ['boost_activated'],
]);
// Inactive but subscribed
$this->createWebhook([
'name' => 'Inactive Subscribed',
'url' => 'https://example.com/webhook-3',
'is_active' => false,
]);
$event = new LimitWarningEvent(
workspace: $this->workspace,
feature: $feature,
used: 80,
limit: 100,
);
$results = $this->service->dispatch($this->workspace, $event, async: true);
$this->assertCount(1, $results);
Bus::assertDispatchedTimes(DispatchEntitlementWebhook::class, 1);
}
public function test_it_returns_empty_results_when_no_webhooks_match(): void
{
Bus::fake([DispatchEntitlementWebhook::class]);
$feature = $this->createFeature();
$event = new LimitWarningEvent(
workspace: $this->workspace,
feature: $feature,
used: 80,
limit: 100,
);
$results = $this->service->dispatch($this->workspace, $event);
$this->assertEmpty($results);
Bus::assertNotDispatched(DispatchEntitlementWebhook::class);
}
public function test_it_handles_synchronous_dispatch_failure_gracefully(): void
{
Http::fake([
'https://example.com/webhook' => Http::response(['error' => 'Server Error'], 500),
]);
$feature = $this->createFeature();
$webhook = $this->createWebhook();
$event = new LimitWarningEvent(
workspace: $this->workspace,
feature: $feature,
used: 80,
limit: 100,
);
$results = $this->service->dispatch($this->workspace, $event, async: false);
$this->assertCount(1, $results);
$this->assertSame($webhook->id, $results[0]['webhook_id']);
$this->assertFalse($results[0]['success']);
}
// -------------------------------------------------------------------------
// Test Webhook
// -------------------------------------------------------------------------
public function test_it_sends_a_test_webhook_delivery(): void
{
Http::fake([
'https://example.com/webhook' => Http::response(['received' => true], 200),
]);
$webhook = $this->createWebhook();
$delivery = $this->service->testWebhook($webhook);
$this->assertInstanceOf(EntitlementWebhookDelivery::class, $delivery);
$this->assertSame('test', $delivery->event);
$this->assertSame(WebhookDeliveryStatus::SUCCESS, $delivery->status);
$this->assertSame(200, $delivery->http_status);
$this->assertSame('test', $delivery->payload['event']);
$this->assertSame($webhook->id, $delivery->payload['data']['webhook_id']);
Http::assertSent(function ($request) {
return $request->hasHeader('X-Signature')
&& $request->hasHeader('X-Test-Webhook');
});
}
public function test_it_records_failed_test_webhook_delivery(): void
{
Http::fake([
'https://example.com/webhook' => Http::response(['error' => 'Bad'], 500),
]);
$webhook = $this->createWebhook();
$delivery = $this->service->testWebhook($webhook);
$this->assertSame(WebhookDeliveryStatus::FAILED, $delivery->status);
$this->assertSame(500, $delivery->http_status);
}
public function test_it_records_delivery_when_test_webhook_throws_exception(): void
{
Http::fake(fn () => throw new \RuntimeException('Connection timed out'));
$webhook = $this->createWebhook();
$delivery = $this->service->testWebhook($webhook);
$this->assertSame(WebhookDeliveryStatus::FAILED, $delivery->status);
$this->assertArrayHasKey('error', $delivery->response);
}
// -------------------------------------------------------------------------
// Retry Delivery
// -------------------------------------------------------------------------
public function test_it_retries_a_failed_delivery_successfully(): void
{
Http::fake([
'https://example.com/webhook' => Http::response(['ok' => true], 200),
]);
$webhook = $this->createWebhook(['failure_count' => 1]);
$delivery = $this->createDelivery($webhook);
$retried = $this->service->retryDelivery($delivery);
$this->assertSame(WebhookDeliveryStatus::SUCCESS, $retried->status);
$this->assertSame(2, $retried->attempts);
$this->assertTrue($retried->resent_manually);
$this->assertSame(200, $retried->http_status);
// Failure count should be reset on success
$webhook->refresh();
$this->assertSame(0, $webhook->failure_count);
}
public function test_it_increments_failure_count_when_retry_fails(): void
{
Http::fake([
'https://example.com/webhook' => Http::response(['error' => 'Still failing'], 502),
]);
$webhook = $this->createWebhook(['failure_count' => 1]);
$delivery = $this->createDelivery($webhook);
$retried = $this->service->retryDelivery($delivery);
$this->assertSame(WebhookDeliveryStatus::FAILED, $retried->status);
$this->assertSame(2, $retried->attempts);
$this->assertTrue($retried->resent_manually);
$webhook->refresh();
$this->assertSame(2, $webhook->failure_count);
}
public function test_it_throws_when_retrying_delivery_for_inactive_webhook(): void
{
$webhook = $this->createWebhook(['is_active' => false]);
$delivery = $this->createDelivery($webhook);
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Cannot retry delivery for inactive webhook');
$this->service->retryDelivery($delivery);
}
public function test_it_handles_exception_during_retry_gracefully(): void
{
Http::fake(fn () => throw new \RuntimeException('Connection timed out'));
$webhook = $this->createWebhook(['failure_count' => 0]);
$delivery = $this->createDelivery($webhook);
$retried = $this->service->retryDelivery($delivery);
$this->assertSame(WebhookDeliveryStatus::FAILED, $retried->status);
$this->assertSame(2, $retried->attempts);
$this->assertTrue($retried->resent_manually);
$this->assertArrayHasKey('error', $retried->response);
$webhook->refresh();
$this->assertSame(1, $webhook->failure_count);
}
// -------------------------------------------------------------------------
// Circuit Breaker
// -------------------------------------------------------------------------
public function test_it_auto_disables_webhook_after_reaching_max_failures(): void
{
$webhook = $this->createWebhook([
'failure_count' => EntitlementWebhook::MAX_FAILURES - 1,
]);
// One more failure should trigger the circuit breaker
$webhook->incrementFailureCount();
$webhook->refresh();
$this->assertFalse($webhook->is_active);
$this->assertTrue($webhook->isCircuitBroken());
$this->assertSame(EntitlementWebhook::MAX_FAILURES, $webhook->failure_count);
}
public function test_it_resets_the_circuit_breaker(): void
{
$webhook = $this->createWebhook([
'is_active' => false,
'failure_count' => EntitlementWebhook::MAX_FAILURES,
]);
$this->assertTrue($webhook->isCircuitBroken());
$this->service->resetCircuitBreaker($webhook);
$webhook->refresh();
$this->assertTrue($webhook->is_active);
$this->assertSame(0, $webhook->failure_count);
$this->assertFalse($webhook->isCircuitBroken());
}
public function test_it_resets_failure_count_on_successful_delivery(): void
{
Http::fake([
'https://example.com/webhook' => Http::response(['ok' => true], 200),
]);
$webhook = $this->createWebhook(['failure_count' => 3]);
$delivery = $this->createDelivery($webhook);
$retried = $this->service->retryDelivery($delivery);
$this->assertSame(WebhookDeliveryStatus::SUCCESS, $retried->status);
$webhook->refresh();
$this->assertSame(0, $webhook->failure_count);
}
// -------------------------------------------------------------------------
// SSRF Protection
// -------------------------------------------------------------------------
public function test_it_blocks_webhook_registration_with_http_url(): void
{
$this->expectException(InvalidWebhookUrlException::class);
$this->service->register(
workspace: $this->workspace,
name: 'HTTP Webhook',
url: 'http://example.com/webhook',
events: ['limit_warning'],
);
}
public function test_it_blocks_webhook_registration_with_localhost_url(): void
{
$this->expectException(InvalidWebhookUrlException::class);
$this->service->register(
workspace: $this->workspace,
name: 'Localhost Webhook',
url: 'https://localhost/webhook',
events: ['limit_warning'],
);
}
public function test_it_blocks_webhook_registration_with_private_ip_url(): void
{
$this->expectException(InvalidWebhookUrlException::class);
$this->service->register(
workspace: $this->workspace,
name: 'Private IP Webhook',
url: 'https://192.168.1.1/webhook',
events: ['limit_warning'],
);
}
public function test_it_blocks_webhook_registration_with_10_range_url(): void
{
$this->expectException(InvalidWebhookUrlException::class);
$this->service->register(
workspace: $this->workspace,
name: 'Private 10 Webhook',
url: 'https://10.0.0.1/webhook',
events: ['limit_warning'],
);
}
public function test_it_blocks_webhook_registration_with_loopback_ip_url(): void
{
$this->expectException(InvalidWebhookUrlException::class);
$this->service->register(
workspace: $this->workspace,
name: 'Loopback Webhook',
url: 'https://127.0.0.1/webhook',
events: ['limit_warning'],
);
}
public function test_it_blocks_webhook_registration_with_local_domain(): void
{
$this->expectException(InvalidWebhookUrlException::class);
$this->service->register(
workspace: $this->workspace,
name: 'Local Domain Webhook',
url: 'https://myserver.local/webhook',
events: ['limit_warning'],
);
}
public function test_it_blocks_webhook_registration_with_internal_domain(): void
{
$this->expectException(InvalidWebhookUrlException::class);
$this->service->register(
workspace: $this->workspace,
name: 'Internal Domain Webhook',
url: 'https://api.internal/webhook',
events: ['limit_warning'],
);
}
public function test_it_blocks_webhook_registration_with_localhost_tld(): void
{
$this->expectException(InvalidWebhookUrlException::class);
$this->service->register(
workspace: $this->workspace,
name: 'Localhost TLD Webhook',
url: 'https://app.localhost/webhook',
events: ['limit_warning'],
);
}
public function test_it_blocks_webhook_update_with_ssrf_url(): void
{
$webhook = $this->service->register(
workspace: $this->workspace,
name: 'SSRF Update Test',
url: 'https://example.com/webhook',
events: ['limit_warning'],
);
$this->expectException(InvalidWebhookUrlException::class);
$this->service->update($webhook, [
'url' => 'https://192.168.0.1/evil',
]);
}
public function test_it_blocks_test_webhook_with_ssrf_url(): void
{
// Directly create a webhook with a private URL to test the SSRF guard
$webhook = $this->createWebhook(['url' => 'https://10.0.0.1/webhook']);
$this->expectException(InvalidWebhookUrlException::class);
$this->service->testWebhook($webhook);
}
public function test_it_blocks_retry_with_ssrf_url(): void
{
$webhook = $this->createWebhook(['url' => 'https://127.0.0.1/webhook']);
$delivery = $this->createDelivery($webhook);
$this->expectException(InvalidWebhookUrlException::class);
$this->service->retryDelivery($delivery);
}
public function test_it_allows_trusted_webhook_domains(): void
{
$webhook = $this->service->register(
workspace: $this->workspace,
name: 'Discord Webhook',
url: 'https://discord.com/api/webhooks/123/abc',
events: ['limit_warning'],
);
$this->assertSame('https://discord.com/api/webhooks/123/abc', $webhook->url);
}
public function test_it_allows_trusted_subdomain_of_webhook_domains(): void
{
$webhook = $this->service->register(
workspace: $this->workspace,
name: 'Slack Webhook',
url: 'https://hooks.slack.com/services/T00/B00/xxx',
events: ['limit_warning'],
);
$this->assertSame('https://hooks.slack.com/services/T00/B00/xxx', $webhook->url);
}
// -------------------------------------------------------------------------
// Available Events
// -------------------------------------------------------------------------
public function test_it_returns_all_available_webhook_event_types(): void
{
$events = $this->service->getAvailableEvents();
$this->assertArrayHasKey('limit_warning', $events);
$this->assertArrayHasKey('limit_reached', $events);
$this->assertArrayHasKey('package_changed', $events);
$this->assertArrayHasKey('boost_activated', $events);
$this->assertArrayHasKey('boost_expired', $events);
$this->assertArrayHasKey('name', $events['limit_warning']);
$this->assertArrayHasKey('description', $events['limit_warning']);
$this->assertArrayHasKey('class', $events['limit_warning']);
$this->assertSame(LimitWarningEvent::class, $events['limit_warning']['class']);
}
public function test_it_returns_event_options_as_key_value_pairs(): void
{
$options = $this->service->getEventOptions();
$this->assertIsArray($options);
$this->assertCount(5, $options);
$this->assertArrayHasKey('limit_warning', $options);
$this->assertArrayHasKey('limit_reached', $options);
$this->assertArrayHasKey('package_changed', $options);
$this->assertArrayHasKey('boost_activated', $options);
$this->assertArrayHasKey('boost_expired', $options);
}
// -------------------------------------------------------------------------
// Workspace Queries
// -------------------------------------------------------------------------
public function test_it_retrieves_webhooks_for_a_workspace(): void
{
$this->createWebhook(['name' => 'Webhook A', 'url' => 'https://example.com/a']);
$this->createWebhook(['name' => 'Webhook B', 'url' => 'https://example.com/b', 'events' => ['boost_activated']]);
// Webhook for a different workspace
$otherWorkspace = Workspace::create([
'name' => 'Other Workspace',
'slug' => 'other-workspace',
'type' => 'cms',
'is_active' => true,
'sort_order' => 2,
]);
EntitlementWebhook::create([
'workspace_id' => $otherWorkspace->id,
'name' => 'Other Workspace Webhook',
'url' => 'https://example.com/other',
'secret' => 'secret',
'events' => ['limit_warning'],
'is_active' => true,
'max_attempts' => 3,
]);
$webhooks = $this->service->getWebhooksForWorkspace($this->workspace);
$this->assertCount(2, $webhooks);
$names = $webhooks->pluck('name')->toArray();
$this->assertContains('Webhook A', $names);
$this->assertContains('Webhook B', $names);
$this->assertNotContains('Other Workspace Webhook', $names);
}
public function test_it_retrieves_delivery_history_for_a_webhook(): void
{
$webhook = $this->createWebhook();
for ($i = 0; $i < 5; $i++) {
$webhook->deliveries()->create([
'uuid' => Str::uuid(),
'event' => 'limit_warning',
'status' => $i % 2 === 0 ? WebhookDeliveryStatus::SUCCESS : WebhookDeliveryStatus::FAILED,
'payload' => ['event' => 'limit_warning', 'data' => ['index' => $i]],
'created_at' => now()->subMinutes(5 - $i),
]);
}
$history = $this->service->getDeliveryHistory($webhook);
$this->assertCount(5, $history);
}
public function test_it_limits_delivery_history_results(): void
{
$webhook = $this->createWebhook();
for ($i = 0; $i < 10; $i++) {
$webhook->deliveries()->create([
'uuid' => Str::uuid(),
'event' => 'limit_warning',
'status' => WebhookDeliveryStatus::SUCCESS,
'payload' => ['event' => 'limit_warning', 'data' => ['index' => $i]],
'created_at' => now()->subMinutes(10 - $i),
]);
}
$history = $this->service->getDeliveryHistory($webhook, limit: 3);
$this->assertCount(3, $history);
}
}