]> BookStack Code Mirror - bookstack/blob - tests/Api/AttachmentsApiTest.php
Added tests for not-yet-built role API endpoints
[bookstack] / tests / Api / AttachmentsApiTest.php
1 <?php
2
3 namespace Tests\Api;
4
5 use BookStack\Entities\Models\Page;
6 use BookStack\Uploads\Attachment;
7 use Illuminate\Http\UploadedFile;
8 use Illuminate\Testing\AssertableJsonString;
9 use Tests\TestCase;
10
11 class AttachmentsApiTest extends TestCase
12 {
13     use TestsApi;
14
15     protected $baseEndpoint = '/api/attachments';
16
17     public function test_index_endpoint_returns_expected_book()
18     {
19         $this->actingAsApiEditor();
20         $page = $this->entities->page();
21         $attachment = $this->createAttachmentForPage($page, [
22             'name'     => 'My test attachment',
23             'external' => true,
24         ]);
25
26         $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');
27         $resp->assertJson(['data' => [
28             [
29                 'id'          => $attachment->id,
30                 'name'        => 'My test attachment',
31                 'uploaded_to' => $page->id,
32                 'external'    => true,
33             ],
34         ]]);
35     }
36
37     public function test_attachments_listing_based_upon_page_visibility()
38     {
39         $this->actingAsApiEditor();
40         $page = $this->entities->page();
41         $attachment = $this->createAttachmentForPage($page, [
42             'name'     => 'My test attachment',
43             'external' => true,
44         ]);
45
46         $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');
47         $resp->assertJson(['data' => [
48             [
49                 'id' => $attachment->id,
50             ],
51         ]]);
52
53         $this->permissions->setEntityPermissions($page, [], []);
54
55         $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');
56         $resp->assertJsonMissing(['data' => [
57             [
58                 'id' => $attachment->id,
59             ],
60         ]]);
61     }
62
63     public function test_create_endpoint_for_link_attachment()
64     {
65         $this->actingAsApiAdmin();
66         $page = $this->entities->page();
67
68         $details = [
69             'name'        => 'My attachment',
70             'uploaded_to' => $page->id,
71             'link'        => 'https://cats.example.com',
72         ];
73
74         $resp = $this->postJson($this->baseEndpoint, $details);
75         $resp->assertStatus(200);
76         /** @var Attachment $newItem */
77         $newItem = Attachment::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
78         $resp->assertJson(['id' => $newItem->id, 'external' => true, 'name' => $details['name'], 'uploaded_to' => $page->id]);
79     }
80
81     public function test_create_endpoint_for_upload_attachment()
82     {
83         $this->actingAsApiAdmin();
84         $page = $this->entities->page();
85         $file = $this->getTestFile('textfile.txt');
86
87         $details = [
88             'name'        => 'My attachment',
89             'uploaded_to' => $page->id,
90         ];
91
92         $resp = $this->call('POST', $this->baseEndpoint, $details, [], ['file' => $file]);
93         $resp->assertStatus(200);
94         /** @var Attachment $newItem */
95         $newItem = Attachment::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
96         $resp->assertJson(['id' => $newItem->id, 'external' => false, 'extension' => 'txt', 'name' => $details['name'], 'uploaded_to' => $page->id]);
97         $this->assertTrue(file_exists(storage_path($newItem->path)));
98         unlink(storage_path($newItem->path));
99     }
100
101     public function test_upload_limit_restricts_attachment_uploads()
102     {
103         $this->actingAsApiAdmin();
104         $page = $this->entities->page();
105
106         config()->set('app.upload_limit', 1);
107
108         $file = tmpfile();
109         $filePath = stream_get_meta_data($file)['uri'];
110         fwrite($file, str_repeat('a', 1200000));
111         $file = new UploadedFile($filePath, 'test.txt', 'text/plain', null, true);
112
113         $details = [
114             'name'        => 'My attachment',
115             'uploaded_to' => $page->id,
116         ];
117         $resp = $this->call('POST', $this->baseEndpoint, $details, [], ['file' => $file]);
118         $resp->assertStatus(422);
119         $resp->assertJson($this->validationResponse([
120             'file' => ['The file may not be greater than 1000 kilobytes.'],
121         ]));
122     }
123
124     public function test_name_needed_to_create()
125     {
126         $this->actingAsApiAdmin();
127         $page = $this->entities->page();
128
129         $details = [
130             'uploaded_to' => $page->id,
131             'link'        => 'https://example.com',
132         ];
133
134         $resp = $this->postJson($this->baseEndpoint, $details);
135         $resp->assertStatus(422);
136         $resp->assertJson($this->validationResponse(['name' => ['The name field is required.']]));
137     }
138
139     public function test_link_or_file_needed_to_create()
140     {
141         $this->actingAsApiAdmin();
142         $page = $this->entities->page();
143
144         $details = [
145             'name'        => 'my attachment',
146             'uploaded_to' => $page->id,
147         ];
148
149         $resp = $this->postJson($this->baseEndpoint, $details);
150         $resp->assertStatus(422);
151         $resp->assertJson($this->validationResponse([
152             'file' => ['The file field is required when link is not present.'],
153             'link' => ['The link field is required when file is not present.'],
154         ]));
155     }
156
157     public function test_message_shown_if_file_is_not_a_valid_file()
158     {
159         $this->actingAsApiAdmin();
160         $page = $this->entities->page();
161
162         $details = [
163             'name'        => 'my attachment',
164             'uploaded_to' => $page->id,
165             'file'        => 'cat',
166         ];
167
168         $resp = $this->postJson($this->baseEndpoint, $details);
169         $resp->assertStatus(422);
170         $resp->assertJson($this->validationResponse(['file' => ['The file must be provided as a valid file.']]));
171     }
172
173     public function test_read_endpoint_for_link_attachment()
174     {
175         $this->actingAsApiAdmin();
176         $page = $this->entities->page();
177
178         $attachment = $this->createAttachmentForPage($page, [
179             'name'  => 'my attachment',
180             'path'  => 'https://example.com',
181             'order' => 1,
182         ]);
183
184         $resp = $this->getJson("{$this->baseEndpoint}/{$attachment->id}");
185
186         $resp->assertStatus(200);
187         $resp->assertJson([
188             'id'          => $attachment->id,
189             'content'     => 'https://example.com',
190             'external'    => true,
191             'uploaded_to' => $page->id,
192             'order'       => 1,
193             'created_by'  => [
194                 'name' => $attachment->createdBy->name,
195             ],
196             'updated_by' => [
197                 'name' => $attachment->createdBy->name,
198             ],
199             'links' => [
200                 'html'     => "<a target=\"_blank\" href=\"http://localhost/attachments/{$attachment->id}\">my attachment</a>",
201                 'markdown' => "[my attachment](http://localhost/attachments/{$attachment->id})",
202             ],
203         ]);
204     }
205
206     public function test_read_endpoint_for_file_attachment()
207     {
208         $this->actingAsApiAdmin();
209         $page = $this->entities->page();
210         $file = $this->getTestFile('textfile.txt');
211
212         $details = [
213             'name'        => 'My file attachment',
214             'uploaded_to' => $page->id,
215         ];
216         $this->call('POST', $this->baseEndpoint, $details, [], ['file' => $file]);
217         /** @var Attachment $attachment */
218         $attachment = Attachment::query()->orderByDesc('id')->where('name', '=', $details['name'])->firstOrFail();
219
220         $resp = $this->getJson("{$this->baseEndpoint}/{$attachment->id}");
221         $resp->assertStatus(200);
222         $resp->assertHeader('Content-Type', 'application/json');
223
224         $json = new AssertableJsonString($resp->streamedContent());
225         $json->assertSubset([
226             'id'          => $attachment->id,
227             'content'     => base64_encode(file_get_contents(storage_path($attachment->path))),
228             'external'    => false,
229             'uploaded_to' => $page->id,
230             'order'       => 1,
231             'created_by'  => [
232                 'name' => $attachment->createdBy->name,
233             ],
234             'updated_by' => [
235                 'name' => $attachment->updatedBy->name,
236             ],
237             'links' => [
238                 'html'     => "<a target=\"_blank\" href=\"http://localhost/attachments/{$attachment->id}\">My file attachment</a>",
239                 'markdown' => "[My file attachment](http://localhost/attachments/{$attachment->id})",
240             ],
241         ]);
242
243         unlink(storage_path($attachment->path));
244     }
245
246     public function test_attachment_not_visible_on_other_users_draft()
247     {
248         $this->actingAsApiAdmin();
249         $editor = $this->users->editor();
250
251         $page = $this->entities->page();
252         $page->draft = true;
253         $page->owned_by = $editor->id;
254         $page->save();
255         $this->permissions->regenerateForEntity($page);
256
257         $attachment = $this->createAttachmentForPage($page, [
258             'name'  => 'my attachment',
259             'path'  => 'https://example.com',
260             'order' => 1,
261         ]);
262
263         $resp = $this->getJson("{$this->baseEndpoint}/{$attachment->id}");
264
265         $resp->assertStatus(404);
266     }
267
268     public function test_update_endpoint()
269     {
270         $this->actingAsApiAdmin();
271         $page = $this->entities->page();
272         $attachment = $this->createAttachmentForPage($page);
273
274         $details = [
275             'name' => 'My updated API attachment',
276         ];
277
278         $resp = $this->putJson("{$this->baseEndpoint}/{$attachment->id}", $details);
279         $attachment->refresh();
280
281         $resp->assertStatus(200);
282         $resp->assertJson(['id' => $attachment->id, 'name' => 'My updated API attachment']);
283     }
284
285     public function test_update_link_attachment_to_file()
286     {
287         $this->actingAsApiAdmin();
288         $page = $this->entities->page();
289         $attachment = $this->createAttachmentForPage($page);
290         $file = $this->getTestFile('textfile.txt');
291
292         $resp = $this->call('PUT', "{$this->baseEndpoint}/{$attachment->id}", ['name' => 'My updated file'], [], ['file' => $file]);
293         $resp->assertStatus(200);
294
295         $attachment->refresh();
296         $this->assertFalse($attachment->external);
297         $this->assertEquals('txt', $attachment->extension);
298         $this->assertStringStartsWith('uploads/files/', $attachment->path);
299         $this->assertFileExists(storage_path($attachment->path));
300
301         unlink(storage_path($attachment->path));
302     }
303
304     public function test_update_file_attachment_to_link()
305     {
306         $this->actingAsApiAdmin();
307         $page = $this->entities->page();
308         $file = $this->getTestFile('textfile.txt');
309         $this->call('POST', $this->baseEndpoint, ['name' => 'My file attachment', 'uploaded_to' => $page->id], [], ['file' => $file]);
310         /** @var Attachment $attachment */
311         $attachment = Attachment::query()->where('name', '=', 'My file attachment')->firstOrFail();
312
313         $filePath = storage_path($attachment->path);
314         $this->assertFileExists($filePath);
315
316         $details = [
317             'name' => 'My updated API attachment',
318             'link' => 'https://cats.example.com',
319         ];
320
321         $resp = $this->putJson("{$this->baseEndpoint}/{$attachment->id}", $details);
322         $resp->assertStatus(200);
323         $attachment->refresh();
324
325         $this->assertFileDoesNotExist($filePath);
326         $this->assertTrue($attachment->external);
327         $this->assertEquals('https://cats.example.com', $attachment->path);
328         $this->assertEquals('', $attachment->extension);
329     }
330
331     public function test_delete_endpoint()
332     {
333         $this->actingAsApiAdmin();
334         $page = $this->entities->page();
335         $attachment = $this->createAttachmentForPage($page);
336
337         $resp = $this->deleteJson("{$this->baseEndpoint}/{$attachment->id}");
338
339         $resp->assertStatus(204);
340         $this->assertDatabaseMissing('attachments', ['id' => $attachment->id]);
341     }
342
343     protected function createAttachmentForPage(Page $page, $attributes = []): Attachment
344     {
345         $admin = $this->users->admin();
346         /** @var Attachment $attachment */
347         $attachment = $page->attachments()->forceCreate(array_merge([
348             'uploaded_to' => $page->id,
349             'name'        => 'test attachment',
350             'external'    => true,
351             'order'       => 1,
352             'created_by'  => $admin->id,
353             'updated_by'  => $admin->id,
354             'path'        => 'https://attachment.example.com',
355         ], $attributes));
356
357         return $attachment;
358     }
359
360     /**
361      * Get a test file that can be uploaded.
362      */
363     protected function getTestFile(string $fileName): UploadedFile
364     {
365         return new UploadedFile(base_path('tests/test-data/test-file.txt'), $fileName, 'text/plain', null, true);
366     }
367 }