]> BookStack Code Mirror - bookstack/blob - tests/Api/BooksApiTest.php
Page Templates: Changed template field name, added API support
[bookstack] / tests / Api / BooksApiTest.php
1 <?php
2
3 namespace Tests\Api;
4
5 use BookStack\Entities\Models\Book;
6 use Carbon\Carbon;
7 use Illuminate\Support\Facades\DB;
8 use Tests\TestCase;
9
10 class BooksApiTest extends TestCase
11 {
12     use TestsApi;
13
14     protected string $baseEndpoint = '/api/books';
15
16     public function test_index_endpoint_returns_expected_book()
17     {
18         $this->actingAsApiEditor();
19         $firstBook = Book::query()->orderBy('id', 'asc')->first();
20
21         $resp = $this->getJson($this->baseEndpoint . '?count=1&sort=+id');
22         $resp->assertJson(['data' => [
23             [
24                 'id'   => $firstBook->id,
25                 'name' => $firstBook->name,
26                 'slug' => $firstBook->slug,
27             ],
28         ]]);
29     }
30
31     public function test_create_endpoint()
32     {
33         $this->actingAsApiEditor();
34         $templatePage = $this->entities->templatePage();
35         $details = [
36             'name'        => 'My API book',
37             'description' => 'A book created via the API',
38             'default_template_id' => $templatePage->id,
39         ];
40
41         $resp = $this->postJson($this->baseEndpoint, $details);
42         $resp->assertStatus(200);
43
44         $newItem = Book::query()->orderByDesc('id')->where('name', '=', $details['name'])->first();
45         $resp->assertJson(array_merge($details, ['id' => $newItem->id, 'slug' => $newItem->slug]));
46         $this->assertActivityExists('book_create', $newItem);
47     }
48
49     public function test_book_name_needed_to_create()
50     {
51         $this->actingAsApiEditor();
52         $details = [
53             'description' => 'A book created via the API',
54         ];
55
56         $resp = $this->postJson($this->baseEndpoint, $details);
57         $resp->assertStatus(422);
58         $resp->assertJson([
59             'error' => [
60                 'message'    => 'The given data was invalid.',
61                 'validation' => [
62                     'name' => ['The name field is required.'],
63                 ],
64                 'code' => 422,
65             ],
66         ]);
67     }
68
69     public function test_read_endpoint()
70     {
71         $this->actingAsApiEditor();
72         $book = $this->entities->book();
73
74         $resp = $this->getJson($this->baseEndpoint . "/{$book->id}");
75
76         $resp->assertStatus(200);
77         $resp->assertJson([
78             'id'         => $book->id,
79             'slug'       => $book->slug,
80             'created_by' => [
81                 'name' => $book->createdBy->name,
82             ],
83             'updated_by' => [
84                 'name' => $book->createdBy->name,
85             ],
86             'owned_by' => [
87                 'name' => $book->ownedBy->name,
88             ],
89             'default_template_id' => null,
90         ]);
91     }
92
93     public function test_read_endpoint_includes_chapter_and_page_contents()
94     {
95         $this->actingAsApiEditor();
96         $book = $this->entities->bookHasChaptersAndPages();
97         $chapter = $book->chapters()->first();
98         $chapterPage = $chapter->pages()->first();
99
100         $resp = $this->getJson($this->baseEndpoint . "/{$book->id}");
101
102         $directChildCount = $book->directPages()->count() + $book->chapters()->count();
103         $resp->assertStatus(200);
104         $resp->assertJsonCount($directChildCount, 'contents');
105         $resp->assertJson([
106             'contents' => [
107                 [
108                     'type' => 'chapter',
109                     'id' => $chapter->id,
110                     'name' => $chapter->name,
111                     'slug' => $chapter->slug,
112                     'pages' => [
113                         [
114                             'id' => $chapterPage->id,
115                             'name' => $chapterPage->name,
116                             'slug' => $chapterPage->slug,
117                         ]
118                     ]
119                 ]
120             ]
121         ]);
122     }
123
124     public function test_update_endpoint()
125     {
126         $this->actingAsApiEditor();
127         $book = $this->entities->book();
128         $templatePage = $this->entities->templatePage();
129         $details = [
130             'name'        => 'My updated API book',
131             'description' => 'A book created via the API',
132             'default_template_id' => $templatePage->id,
133         ];
134
135         $resp = $this->putJson($this->baseEndpoint . "/{$book->id}", $details);
136         $book->refresh();
137
138         $resp->assertStatus(200);
139         $resp->assertJson(array_merge($details, ['id' => $book->id, 'slug' => $book->slug]));
140         $this->assertActivityExists('book_update', $book);
141     }
142
143     public function test_update_increments_updated_date_if_only_tags_are_sent()
144     {
145         $this->actingAsApiEditor();
146         $book = $this->entities->book();
147         DB::table('books')->where('id', '=', $book->id)->update(['updated_at' => Carbon::now()->subWeek()]);
148
149         $details = [
150             'tags' => [['name' => 'Category', 'value' => 'Testing']],
151         ];
152
153         $this->putJson($this->baseEndpoint . "/{$book->id}", $details);
154         $book->refresh();
155         $this->assertGreaterThan(Carbon::now()->subDay()->unix(), $book->updated_at->unix());
156     }
157
158     public function test_update_cover_image_control()
159     {
160         $this->actingAsApiEditor();
161         /** @var Book $book */
162         $book = $this->entities->book();
163         $this->assertNull($book->cover);
164         $file = $this->files->uploadedImage('image.png');
165
166         // Ensure cover image can be set via API
167         $resp = $this->call('PUT', $this->baseEndpoint . "/{$book->id}", [
168             'name'  => 'My updated API book with image',
169         ], [], ['image' => $file]);
170         $book->refresh();
171
172         $resp->assertStatus(200);
173         $this->assertNotNull($book->cover);
174
175         // Ensure further updates without image do not clear cover image
176         $resp = $this->put($this->baseEndpoint . "/{$book->id}", [
177             'name' => 'My updated book again',
178         ]);
179         $book->refresh();
180
181         $resp->assertStatus(200);
182         $this->assertNotNull($book->cover);
183
184         // Ensure update with null image property clears image
185         $resp = $this->put($this->baseEndpoint . "/{$book->id}", [
186             'image' => null,
187         ]);
188         $book->refresh();
189
190         $resp->assertStatus(200);
191         $this->assertNull($book->cover);
192     }
193
194     public function test_delete_endpoint()
195     {
196         $this->actingAsApiEditor();
197         $book = $this->entities->book();
198         $resp = $this->deleteJson($this->baseEndpoint . "/{$book->id}");
199
200         $resp->assertStatus(204);
201         $this->assertActivityExists('book_delete');
202     }
203
204     public function test_export_html_endpoint()
205     {
206         $this->actingAsApiEditor();
207         $book = $this->entities->book();
208
209         $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/html");
210         $resp->assertStatus(200);
211         $resp->assertSee($book->name);
212         $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.html"');
213     }
214
215     public function test_export_plain_text_endpoint()
216     {
217         $this->actingAsApiEditor();
218         $book = $this->entities->book();
219
220         $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/plaintext");
221         $resp->assertStatus(200);
222         $resp->assertSee($book->name);
223         $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.txt"');
224     }
225
226     public function test_export_pdf_endpoint()
227     {
228         $this->actingAsApiEditor();
229         $book = $this->entities->book();
230
231         $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/pdf");
232         $resp->assertStatus(200);
233         $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.pdf"');
234     }
235
236     public function test_export_markdown_endpoint()
237     {
238         $this->actingAsApiEditor();
239         $book = Book::visible()->has('pages')->has('chapters')->first();
240
241         $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/markdown");
242         $resp->assertStatus(200);
243         $resp->assertHeader('Content-Disposition', 'attachment; filename="' . $book->slug . '.md"');
244         $resp->assertSee('# ' . $book->name);
245         $resp->assertSee('# ' . $book->pages()->first()->name);
246         $resp->assertSee('# ' . $book->chapters()->first()->name);
247     }
248
249     public function test_cant_export_when_not_have_permission()
250     {
251         $types = ['html', 'plaintext', 'pdf', 'markdown'];
252         $this->actingAsApiEditor();
253         $this->permissions->removeUserRolePermissions($this->users->editor(), ['content-export']);
254
255         $book = $this->entities->book();
256         foreach ($types as $type) {
257             $resp = $this->get($this->baseEndpoint . "/{$book->id}/export/{$type}");
258             $this->assertPermissionError($resp);
259         }
260     }
261 }