Send a webhook for every change a person makes to a record
Events are {resource}.created, .updated, .deleted and .restored for content,
the inbox, redirects, repositories and media, taken from the audit log so the
two agree on what happened. Subscriptions take wildcards. Users, tokens,
webhooks and settings are never sent, nor what the application does by
itself; the inbox's own words only with personal data enabled.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
158
tests/Feature/Api/WebhookRecordEventsTest.php
Normal file
158
tests/Feature/Api/WebhookRecordEventsTest.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Tests\Feature\Api;
|
||||
|
||||
use App\Enums\ApplicationStatus;
|
||||
use App\Models\Application;
|
||||
use App\Models\Page;
|
||||
use App\Models\Project;
|
||||
use App\Models\Repository;
|
||||
use App\Models\User;
|
||||
use App\Models\Webhook;
|
||||
use App\Models\WebhookDelivery;
|
||||
use App\Services\Media\HostResolver;
|
||||
use Database\Seeders\RolePermissionSeeder;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Illuminate\Support\Facades\Queue;
|
||||
use PHPUnit\Framework\Attributes\Test;
|
||||
use Tests\FakesPireon;
|
||||
use Tests\TestCase;
|
||||
|
||||
/**
|
||||
* `{resource}.{action}`: every change a person makes to a record, as a
|
||||
* webhook event, with wildcards to subscribe broadly.
|
||||
*/
|
||||
class WebhookRecordEventsTest extends TestCase
|
||||
{
|
||||
use FakesPireon, RefreshDatabase;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->seed(RolePermissionSeeder::class);
|
||||
Queue::fake();
|
||||
|
||||
$this->app->instance(HostResolver::class, new class extends HostResolver
|
||||
{
|
||||
public function resolve(string $host): array
|
||||
{
|
||||
return ['93.184.216.34'];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
protected function actingWithRole(string $role): User
|
||||
{
|
||||
$user = User::factory()->create();
|
||||
$user->syncRoles([$role]);
|
||||
|
||||
$this->withHeaders($this->apiHeadersFor($user));
|
||||
|
||||
return $user;
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function wildcards_cover_a_kind_of_record_or_an_action(): void
|
||||
{
|
||||
$webhook = Webhook::factory()->make(['events' => ['projects.*', '*.deleted']]);
|
||||
|
||||
$this->assertTrue($webhook->listensTo('projects.updated'));
|
||||
$this->assertTrue($webhook->listensTo('pages.deleted'));
|
||||
$this->assertFalse($webhook->listensTo('pages.updated'));
|
||||
$this->assertFalse($webhook->listensTo('content.published'));
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function editing_a_project_says_what_changed_who_did_it_and_how(): void
|
||||
{
|
||||
$editor = $this->actingWithRole('editor');
|
||||
|
||||
$webhook = Webhook::factory()->create(['events' => ['projects.*']]);
|
||||
$project = Project::factory()->create(['status' => 'active']);
|
||||
|
||||
$this->patchJson("/api/v1/projects/{$project->id}", ['status' => 'paused'])->assertOk();
|
||||
|
||||
$delivery = $webhook->deliveries()->where('event', 'projects.updated')->sole();
|
||||
|
||||
$this->assertSame($project->id, $delivery->payload['id']);
|
||||
$this->assertSame(['old' => 'active', 'new' => 'paused'], $delivery->payload['changes']['status']);
|
||||
$this->assertSame($editor->id, $delivery->payload['by']['id']);
|
||||
$this->assertSame('api', $delivery->payload['via']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function creating_and_deleting_are_events_too(): void
|
||||
{
|
||||
$this->actingWithRole('editor');
|
||||
|
||||
$webhook = Webhook::factory()->create(['events' => ['pages.created', '*.deleted']]);
|
||||
|
||||
$id = $this->postJson('/api/v1/pages', ['title' => ['es' => 'Hola'], 'slug' => ['es' => 'hola']])->json('data.id');
|
||||
$this->deleteJson("/api/v1/pages/{$id}")->assertNoContent();
|
||||
|
||||
$this->assertSame(['pages.deleted', 'pages.created'], $webhook->deliveries()->pluck('event')->all());
|
||||
$this->assertNull($webhook->deliveries()->first()->payload['url']);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function what_the_application_does_by_itself_is_not_announced(): void
|
||||
{
|
||||
$webhook = Webhook::factory()->create(['events' => ['*']]);
|
||||
|
||||
Repository::factory()->create()->forceFill(['stars' => 10])->save();
|
||||
|
||||
$this->assertSame(0, $webhook->deliveries()->count());
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function security_records_cannot_be_subscribed_to(): void
|
||||
{
|
||||
$this->actingWithRole('admin');
|
||||
|
||||
foreach (['users.updated', 'tokens.*', 'settings.updated', 'webhooks.created', 'projects.exploded'] as $event) {
|
||||
$this->postJson('/api/v1/webhooks', ['name' => 'X', 'url' => 'https://hooks.example.org/x', 'events' => [$event]])
|
||||
->assertUnprocessable()
|
||||
->assertJsonValidationErrors('events.0');
|
||||
}
|
||||
|
||||
$this->postJson('/api/v1/webhooks', ['name' => 'X', 'url' => 'https://hooks.example.org/x', 'events' => ['projects.*', '*.restored']])
|
||||
->assertCreated();
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function the_inbox_tells_how_it_is_handled_and_its_words_only_when_asked(): void
|
||||
{
|
||||
$this->actingWithRole('editor');
|
||||
|
||||
$plain = Webhook::factory()->create(['events' => ['applications.updated']]);
|
||||
$personal = Webhook::factory()->create(['events' => ['applications.*'], 'include_personal_data' => true]);
|
||||
|
||||
$application = Application::factory()->create(['email' => 'ada@example.org']);
|
||||
|
||||
$this->patchJson("/api/v1/applications/{$application->id}", ['status' => 'contacted', 'notes' => 'Llamar el lunes'])->assertOk();
|
||||
|
||||
$sent = $plain->deliveries()->latest('id')->first()->payload;
|
||||
$full = $personal->deliveries()->latest('id')->first()->payload;
|
||||
|
||||
$this->assertArrayNotHasKey('notes', $sent['changes'] ?? []);
|
||||
$this->assertArrayNotHasKey('email', $sent);
|
||||
$this->assertSame('ada@example.org', $full['email']);
|
||||
$this->assertSame(ApplicationStatus::Contacted->value, $application->fresh()->status->value);
|
||||
}
|
||||
|
||||
#[Test]
|
||||
public function the_events_answer_as_queued_deliveries(): void
|
||||
{
|
||||
$this->actingWithRole('editor');
|
||||
|
||||
Webhook::factory()->create(['events' => ['*']]);
|
||||
$page = Page::factory()->create();
|
||||
|
||||
$this->patchJson("/api/v1/pages/{$page->id}", ['is_featured' => ! $page->is_featured])->assertOk();
|
||||
|
||||
$this->assertTrue(WebhookDelivery::query()->where('event', 'pages.updated')->exists());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user