]> BookStack Code Mirror - bookstack/blob - tests/Actions/WebhookCallTest.php
My Account: Updated and started adding to tests
[bookstack] / tests / Actions / WebhookCallTest.php
1 <?php
2
3 namespace Tests\Actions;
4
5 use BookStack\Activity\ActivityType;
6 use BookStack\Activity\DispatchWebhookJob;
7 use BookStack\Activity\Models\Webhook;
8 use BookStack\Activity\Tools\ActivityLogger;
9 use BookStack\Api\ApiToken;
10 use BookStack\Users\Models\User;
11 use GuzzleHttp\Exception\ConnectException;
12 use GuzzleHttp\Psr7\Response;
13 use Illuminate\Support\Facades\Bus;
14 use Tests\TestCase;
15
16 class WebhookCallTest extends TestCase
17 {
18     public function test_webhook_listening_to_all_called_on_event()
19     {
20         $this->newWebhook([], ['all']);
21         Bus::fake();
22         $this->runEvent(ActivityType::ROLE_CREATE);
23         Bus::assertDispatched(DispatchWebhookJob::class);
24     }
25
26     public function test_webhook_listening_to_specific_event_called_on_event()
27     {
28         $this->newWebhook([], [ActivityType::ROLE_UPDATE]);
29         Bus::fake();
30         $this->runEvent(ActivityType::ROLE_UPDATE);
31         Bus::assertDispatched(DispatchWebhookJob::class);
32     }
33
34     public function test_webhook_listening_to_specific_event_not_called_on_other_event()
35     {
36         $this->newWebhook([], [ActivityType::ROLE_UPDATE]);
37         Bus::fake();
38         $this->runEvent(ActivityType::ROLE_CREATE);
39         Bus::assertNotDispatched(DispatchWebhookJob::class);
40     }
41
42     public function test_inactive_webhook_not_called_on_event()
43     {
44         $this->newWebhook(['active' => false], ['all']);
45         Bus::fake();
46         $this->runEvent(ActivityType::ROLE_CREATE);
47         Bus::assertNotDispatched(DispatchWebhookJob::class);
48     }
49
50     public function test_webhook_runs_for_delete_actions()
51     {
52         // This test must not fake the queue/bus since this covers an issue
53         // around handling and serialization of items now deleted from the database.
54         $webhook = $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']);
55         $this->mockHttpClient([new Response(500)]);
56
57         $user = $this->users->newUser();
58         $resp = $this->asAdmin()->delete($user->getEditUrl());
59         $resp->assertRedirect('/settings/users');
60
61         /** @var ApiToken $apiToken */
62         $editor = $this->users->editor();
63         $apiToken = ApiToken::factory()->create(['user_id' => $editor]);
64         $this->delete($apiToken->getUrl())->assertRedirect();
65
66         $webhook->refresh();
67         $this->assertEquals('Response status from endpoint was 500', $webhook->last_error);
68     }
69
70     public function test_failed_webhook_call_logs_error()
71     {
72         $logger = $this->withTestLogger();
73         $this->mockHttpClient([new Response(500)]);
74         $webhook = $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']);
75         $this->assertNull($webhook->last_errored_at);
76
77         $this->runEvent(ActivityType::ROLE_CREATE);
78
79         $this->assertTrue($logger->hasError('Webhook call to endpoint https://wh.example.com failed with status 500'));
80
81         $webhook->refresh();
82         $this->assertEquals('Response status from endpoint was 500', $webhook->last_error);
83         $this->assertNotNull($webhook->last_errored_at);
84     }
85
86     public function test_webhook_call_exception_is_caught_and_logged()
87     {
88         $this->mockHttpClient([new ConnectException('Failed to perform request', new \GuzzleHttp\Psr7\Request('GET', ''))]);
89
90         $logger = $this->withTestLogger();
91         $webhook = $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']);
92         $this->assertNull($webhook->last_errored_at);
93
94         $this->runEvent(ActivityType::ROLE_CREATE);
95
96         $this->assertTrue($logger->hasError('Webhook call to endpoint https://wh.example.com failed with error "Failed to perform request"'));
97
98         $webhook->refresh();
99         $this->assertEquals('Failed to perform request', $webhook->last_error);
100         $this->assertNotNull($webhook->last_errored_at);
101     }
102
103     public function test_webhook_uses_ssr_hosts_option_if_set()
104     {
105         config()->set('app.ssr_hosts', 'https://*.example.com');
106         $responses = $this->mockHttpClient();
107
108         $webhook = $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.co.uk'], ['all']);
109         $this->runEvent(ActivityType::ROLE_CREATE);
110         $this->assertEquals(0, $responses->requestCount());
111
112         $webhook->refresh();
113         $this->assertEquals('The URL does not match the configured allowed SSR hosts', $webhook->last_error);
114         $this->assertNotNull($webhook->last_errored_at);
115     }
116
117     public function test_webhook_call_data_format()
118     {
119         $responses = $this->mockHttpClient([new Response(200, [], '')]);
120         $webhook = $this->newWebhook(['active' => true, 'endpoint' => 'https://wh.example.com'], ['all']);
121         $page = $this->entities->page();
122         $editor = $this->users->editor();
123
124         $this->runEvent(ActivityType::PAGE_UPDATE, $page, $editor);
125
126         $request = $responses->latestRequest();
127         $reqData = json_decode($request->getBody(), true);
128         $this->assertEquals('page_update', $reqData['event']);
129         $this->assertEquals(($editor->name . ' updated page "' . $page->name . '"'), $reqData['text']);
130         $this->assertIsString($reqData['triggered_at']);
131         $this->assertEquals($editor->name, $reqData['triggered_by']['name']);
132         $this->assertEquals($editor->getProfileUrl(), $reqData['triggered_by_profile_url']);
133         $this->assertEquals($webhook->id, $reqData['webhook_id']);
134         $this->assertEquals($webhook->name, $reqData['webhook_name']);
135         $this->assertEquals($page->getUrl(), $reqData['url']);
136         $this->assertEquals($page->name, $reqData['related_item']['name']);
137     }
138
139     protected function runEvent(string $event, $detail = '', ?User $user = null)
140     {
141         if (is_null($user)) {
142             $user = $this->users->editor();
143         }
144
145         $this->actingAs($user);
146
147         $activityLogger = $this->app->make(ActivityLogger::class);
148         $activityLogger->add($event, $detail);
149     }
150
151     protected function newWebhook(array $attrs, array $events): Webhook
152     {
153         /** @var Webhook $webhook */
154         $webhook = Webhook::factory()->create($attrs);
155
156         foreach ($events as $event) {
157             $webhook->trackedEvents()->create(['event' => $event]);
158         }
159
160         return $webhook;
161     }
162 }