diff --git a/app/Actions/DispatchWebhookJob.php b/app/Actions/DispatchWebhookJob.php index 4cc749af3..69d04d36c 100644 --- a/app/Actions/DispatchWebhookJob.php +++ b/app/Actions/DispatchWebhookJob.php @@ -6,16 +6,14 @@ use BookStack\Auth\User; use BookStack\Entities\Models\Entity; use BookStack\Interfaces\Loggable; use BookStack\Model; -use GuzzleHttp\Client; -use GuzzleHttp\Psr7\Request; use Illuminate\Bus\Queueable; use Illuminate\Contracts\Queue\ShouldQueue; use Illuminate\Foundation\Bus\Dispatchable; use Illuminate\Queue\InteractsWithQueue; use Illuminate\Queue\SerializesModels; use Illuminate\Support\Carbon; +use Illuminate\Support\Facades\Http; use Illuminate\Support\Facades\Log; -use Psr\Http\Client\ClientExceptionInterface; class DispatchWebhookJob implements ShouldQueue { @@ -67,22 +65,13 @@ class DispatchWebhookJob implements ShouldQueue */ public function handle() { - $httpClient = new Client([ - 'timeout' => 3, - 'allow_redirects' => ['strict' => true], - ]); + $response = Http::asJson() + ->withOptions(['allow_redirects' => ['strict' => true]]) + ->timeout(3) + ->post($this->webhook->endpoint, $this->buildWebhookData()); - $request = new Request('POST', $this->webhook->endpoint, [ - 'Content-Type' => 'application/json' - ], json_encode($this->buildWebhookData())); - - try { - $response = $httpClient->send($request); - if ($response->getStatusCode() >= 400) { - Log::error("Webhook call to endpoint {$this->webhook->endpoint} failed with status {$response->getStatusCode()}"); - } - } catch (ClientExceptionInterface $exception) { - Log::error("Received error during webhook call to endpoint {$this->webhook->endpoint}: {$exception->getMessage()}"); + if ($response->failed()) { + Log::error("Webhook call to endpoint {$this->webhook->endpoint} failed with status {$response->status()}"); } } diff --git a/tests/Actions/WebhookCallTest.php b/tests/Actions/WebhookCallTest.php new file mode 100644 index 000000000..958d33d9d --- /dev/null +++ b/tests/Actions/WebhookCallTest.php @@ -0,0 +1,116 @@ +newWebhook([], ['all']); + Bus::fake(); + $this->runEvent(ActivityType::ROLE_CREATE); + Bus::assertDispatched(DispatchWebhookJob::class); + } + + public function test_webhook_listening_to_specific_event_called_on_event() + { + $this->newWebhook([], [ActivityType::ROLE_UPDATE]); + Bus::fake(); + $this->runEvent(ActivityType::ROLE_UPDATE); + Bus::assertDispatched(DispatchWebhookJob::class); + } + + public function test_webhook_listening_to_specific_event_not_called_on_other_event() + { + $this->newWebhook([], [ActivityType::ROLE_UPDATE]); + Bus::fake(); + $this->runEvent(ActivityType::ROLE_CREATE); + Bus::assertNotDispatched(DispatchWebhookJob::class); + } + + public function test_inactive_webhook_not_called_on_event() + { + $this->newWebhook(['active' => false], ['all']); + Bus::fake(); + $this->runEvent(ActivityType::ROLE_CREATE); + Bus::assertNotDispatched(DispatchWebhookJob::class); + } + + public function test_failed_webhook_call_logs_error() + { + $logger = $this->withTestLogger(); + Http::fake([ + '*' => Http::response('', 500), + ]); + $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']); + + $this->runEvent(ActivityType::ROLE_CREATE); + + $this->assertTrue($logger->hasError('Webhook call to endpoint https://wh.example.com failed with status 500')); + } + + public function test_webhook_call_data_format() + { + Http::fake([ + '*' => Http::response('', 200), + ]); + $webhook = $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']); + /** @var Page $page */ + $page = Page::query()->first(); + $editor = $this->getEditor(); + + $this->runEvent(ActivityType::PAGE_UPDATE, $page, $editor); + + Http::assertSent(function(Request $request) use ($editor, $page, $webhook) { + $reqData = $request->data(); + return $request->isJson() + && $reqData['event'] === 'page_update' + && $reqData['text'] === ($editor->name . ' updated page "' . $page->name . '"') + && is_string($reqData['triggered_at']) + && $reqData['triggered_by']['name'] === $editor->name + && $reqData['triggered_by_profile_url'] === $editor->getProfileUrl() + && $reqData['webhook_id'] === $webhook->id + && $reqData['webhook_name'] === $webhook->name + && $reqData['url'] === $page->getUrl() + && $reqData['related_item']['name'] === $page->name; + }); + } + + + protected function runEvent(string $event, $detail = '', ?User $user = null) + { + if (is_null($user)) { + $user = $this->getEditor(); + } + + $this->actingAs($user); + + $activityLogger = $this->app->make(ActivityLogger::class); + $activityLogger->add($event, $detail); + } + + protected function newWebhook(array $attrs = [], array $events = ['all']): Webhook + { + /** @var Webhook $webhook */ + $webhook = Webhook::factory()->create($attrs); + + foreach ($events as $event) { + $webhook->trackedEvents()->create(['event' => $event]); + } + + return $webhook; + } + +} \ No newline at end of file