]> BookStack Code Mirror - bookstack/blob - tests/Exports/ZipImportTest.php
ZIP Imports: Added high level import run tests
[bookstack] / tests / Exports / ZipImportTest.php
1 <?php
2
3 namespace Tests\Exports;
4
5 use BookStack\Activity\ActivityType;
6 use BookStack\Entities\Models\Book;
7 use BookStack\Exports\Import;
8 use BookStack\Exports\ZipExports\Models\ZipExportBook;
9 use BookStack\Exports\ZipExports\Models\ZipExportChapter;
10 use BookStack\Exports\ZipExports\Models\ZipExportPage;
11 use Illuminate\Http\UploadedFile;
12 use Illuminate\Testing\TestResponse;
13 use Tests\TestCase;
14 use ZipArchive;
15
16 class ZipImportTest extends TestCase
17 {
18     public function test_import_page_view()
19     {
20         $resp = $this->asAdmin()->get('/import');
21         $resp->assertSee('Import');
22         $this->withHtml($resp)->assertElementExists('form input[type="file"][name="file"]');
23     }
24
25     public function test_permissions_needed_for_import_page()
26     {
27         $user = $this->users->viewer();
28         $this->actingAs($user);
29
30         $resp = $this->get('/books');
31         $this->withHtml($resp)->assertLinkNotExists(url('/import'));
32         $resp = $this->get('/import');
33         $resp->assertRedirect('/');
34
35         $this->permissions->grantUserRolePermissions($user, ['content-import']);
36
37         $resp = $this->get('/books');
38         $this->withHtml($resp)->assertLinkExists(url('/import'));
39         $resp = $this->get('/import');
40         $resp->assertOk();
41         $resp->assertSeeText('Select ZIP file to upload');
42     }
43
44     public function test_import_page_pending_import_visibility_limited()
45     {
46         $user = $this->users->viewer();
47         $admin = $this->users->admin();
48         $userImport = Import::factory()->create(['name' => 'MySuperUserImport', 'created_by' => $user->id]);
49         $adminImport = Import::factory()->create(['name' => 'MySuperAdminImport', 'created_by' => $admin->id]);
50         $this->permissions->grantUserRolePermissions($user, ['content-import']);
51
52         $resp = $this->actingAs($user)->get('/import');
53         $resp->assertSeeText('MySuperUserImport');
54         $resp->assertDontSeeText('MySuperAdminImport');
55
56         $this->permissions->grantUserRolePermissions($user, ['settings-manage']);
57
58         $resp = $this->actingAs($user)->get('/import');
59         $resp->assertSeeText('MySuperUserImport');
60         $resp->assertSeeText('MySuperAdminImport');
61     }
62
63     public function test_zip_read_errors_are_shown_on_validation()
64     {
65         $invalidUpload = $this->files->uploadedImage('image.zip');
66
67         $this->asAdmin();
68         $resp = $this->runImportFromFile($invalidUpload);
69         $resp->assertRedirect('/import');
70
71         $resp = $this->followRedirects($resp);
72         $resp->assertSeeText('Could not read ZIP file');
73     }
74
75     public function test_error_shown_if_missing_data()
76     {
77         $zipFile = tempnam(sys_get_temp_dir(), 'bstest-');
78         $zip = new ZipArchive();
79         $zip->open($zipFile, ZipArchive::CREATE);
80         $zip->addFromString('beans', 'cat');
81         $zip->close();
82
83         $this->asAdmin();
84         $upload = new UploadedFile($zipFile, 'upload.zip', 'application/zip', null, true);
85         $resp = $this->runImportFromFile($upload);
86         $resp->assertRedirect('/import');
87
88         $resp = $this->followRedirects($resp);
89         $resp->assertSeeText('Could not find and decode ZIP data.json content.');
90     }
91
92     public function test_error_shown_if_no_importable_key()
93     {
94         $this->asAdmin();
95         $resp = $this->runImportFromFile(ZipTestHelper::zipUploadFromData([
96             'instance' => []
97         ]));
98
99         $resp->assertRedirect('/import');
100         $resp = $this->followRedirects($resp);
101         $resp->assertSeeText('ZIP file data has no expected book, chapter or page content.');
102     }
103
104     public function test_zip_data_validation_messages_shown()
105     {
106         $this->asAdmin();
107         $resp = $this->runImportFromFile(ZipTestHelper::zipUploadFromData([
108             'book' => [
109                 'id' => 4,
110                 'pages' => [
111                     'cat',
112                     [
113                         'name' => 'My inner page',
114                         'tags' => [
115                             [
116                                 'value' => 5
117                             ]
118                         ],
119                     ]
120                 ],
121             ]
122         ]));
123
124         $resp->assertRedirect('/import');
125         $resp = $this->followRedirects($resp);
126
127         $resp->assertSeeText('[book.name]: The name field is required.');
128         $resp->assertSeeText('[book.pages.0.0]: Data object expected but "string" found.');
129         $resp->assertSeeText('[book.pages.1.tags.0.name]: The name field is required.');
130         $resp->assertSeeText('[book.pages.1.tags.0.value]: The value must be a string.');
131     }
132
133     public function test_import_upload_success()
134     {
135         $admin = $this->users->admin();
136         $this->actingAs($admin);
137         $data = [
138             'book' => [
139                 'name' => 'My great book name',
140                 'chapters' => [
141                     [
142                         'name' => 'my chapter',
143                         'pages' => [
144                             [
145                                 'name' => 'my chapter page',
146                             ]
147                         ]
148                     ]
149                 ],
150                 'pages' => [
151                     [
152                         'name' => 'My page',
153                     ]
154                 ],
155             ],
156         ];
157
158         $resp = $this->runImportFromFile(ZipTestHelper::zipUploadFromData($data));
159
160         $this->assertDatabaseHas('imports', [
161             'name' => 'My great book name',
162             'type' => 'book',
163             'created_by' => $admin->id,
164         ]);
165
166         /** @var Import $import */
167         $import = Import::query()->latest()->first();
168         $resp->assertRedirect("/import/{$import->id}");
169         $this->assertFileExists(storage_path($import->path));
170         $this->assertActivityExists(ActivityType::IMPORT_CREATE);
171     }
172
173     public function test_import_show_page()
174     {
175         $exportBook = new ZipExportBook();
176         $exportBook->name = 'My exported book';
177         $exportChapter = new ZipExportChapter();
178         $exportChapter->name = 'My exported chapter';
179         $exportPage = new ZipExportPage();
180         $exportPage->name = 'My exported page';
181         $exportBook->chapters = [$exportChapter];
182         $exportChapter->pages = [$exportPage];
183
184         $import = Import::factory()->create([
185             'name' => 'MySuperAdminImport',
186             'metadata' => json_encode($exportBook)
187         ]);
188
189         $resp = $this->asAdmin()->get("/import/{$import->id}");
190         $resp->assertOk();
191         $resp->assertSeeText('My exported book');
192         $resp->assertSeeText('My exported chapter');
193         $resp->assertSeeText('My exported page');
194     }
195
196     public function test_import_show_page_access_limited()
197     {
198         $user = $this->users->viewer();
199         $admin = $this->users->admin();
200         $userImport = Import::factory()->create(['name' => 'MySuperUserImport', 'created_by' => $user->id]);
201         $adminImport = Import::factory()->create(['name' => 'MySuperAdminImport', 'created_by' => $admin->id]);
202         $this->actingAs($user);
203
204         $this->get("/import/{$userImport->id}")->assertRedirect('/');
205         $this->get("/import/{$adminImport->id}")->assertRedirect('/');
206
207         $this->permissions->grantUserRolePermissions($user, ['content-import']);
208
209         $this->get("/import/{$userImport->id}")->assertOk();
210         $this->get("/import/{$adminImport->id}")->assertStatus(404);
211
212         $this->permissions->grantUserRolePermissions($user, ['settings-manage']);
213
214         $this->get("/import/{$userImport->id}")->assertOk();
215         $this->get("/import/{$adminImport->id}")->assertOk();
216     }
217
218     public function test_import_delete()
219     {
220         $this->asAdmin();
221         $this->runImportFromFile(ZipTestHelper::zipUploadFromData([
222             'book' => [
223                 'name' => 'My great book name'
224             ],
225         ]));
226
227         /** @var Import $import */
228         $import = Import::query()->latest()->first();
229         $this->assertDatabaseHas('imports', [
230             'id' => $import->id,
231             'name' => 'My great book name'
232         ]);
233         $this->assertFileExists(storage_path($import->path));
234
235         $resp = $this->delete("/import/{$import->id}");
236
237         $resp->assertRedirect('/import');
238         $this->assertActivityExists(ActivityType::IMPORT_DELETE);
239         $this->assertDatabaseMissing('imports', [
240             'id' => $import->id,
241         ]);
242         $this->assertFileDoesNotExist(storage_path($import->path));
243     }
244
245     public function test_import_delete_access_limited()
246     {
247         $user = $this->users->viewer();
248         $admin = $this->users->admin();
249         $userImport = Import::factory()->create(['name' => 'MySuperUserImport', 'created_by' => $user->id]);
250         $adminImport = Import::factory()->create(['name' => 'MySuperAdminImport', 'created_by' => $admin->id]);
251         $this->actingAs($user);
252
253         $this->delete("/import/{$userImport->id}")->assertRedirect('/');
254         $this->delete("/import/{$adminImport->id}")->assertRedirect('/');
255
256         $this->permissions->grantUserRolePermissions($user, ['content-import']);
257
258         $this->delete("/import/{$userImport->id}")->assertRedirect('/import');
259         $this->delete("/import/{$adminImport->id}")->assertStatus(404);
260
261         $this->permissions->grantUserRolePermissions($user, ['settings-manage']);
262
263         $this->delete("/import/{$adminImport->id}")->assertRedirect('/import');
264     }
265
266     public function test_run_simple_success_scenario()
267     {
268         $import = ZipTestHelper::importFromData([], [
269             'book' => [
270                 'name' => 'My imported book',
271                 'pages' => [
272                     [
273                         'name' => 'My imported book page',
274                         'html' => '<p>Hello there from child page!</p>'
275                     ]
276                 ],
277             ]
278         ]);
279
280         $resp = $this->asAdmin()->post("/import/{$import->id}");
281         $book = Book::query()->where('name', '=', 'My imported book')->latest()->first();
282         $resp->assertRedirect($book->getUrl());
283
284         $resp = $this->followRedirects($resp);
285         $resp->assertSee('My imported book page');
286         $resp->assertSee('Hello there from child page!');
287
288         $this->assertDatabaseMissing('imports', ['id' => $import->id]);
289         $this->assertFileDoesNotExist(storage_path($import->path));
290         $this->assertActivityExists(ActivityType::IMPORT_RUN, null, $import->logDescriptor());
291     }
292
293     public function test_import_run_access_limited()
294     {
295         $user = $this->users->editor();
296         $admin = $this->users->admin();
297         $userImport = Import::factory()->create(['name' => 'MySuperUserImport', 'created_by' => $user->id]);
298         $adminImport = Import::factory()->create(['name' => 'MySuperAdminImport', 'created_by' => $admin->id]);
299         $this->actingAs($user);
300
301         $this->post("/import/{$userImport->id}")->assertRedirect('/');
302         $this->post("/import/{$adminImport->id}")->assertRedirect('/');
303
304         $this->permissions->grantUserRolePermissions($user, ['content-import']);
305
306         $this->post("/import/{$userImport->id}")->assertRedirect($userImport->getUrl()); // Getting validation response instead of access issue response
307         $this->post("/import/{$adminImport->id}")->assertStatus(404);
308
309         $this->permissions->grantUserRolePermissions($user, ['settings-manage']);
310
311         $this->post("/import/{$adminImport->id}")->assertRedirect($adminImport->getUrl()); // Getting validation response instead of access issue response
312     }
313
314     public function test_run_revalidates_content()
315     {
316         $import = ZipTestHelper::importFromData([], [
317             'book' => [
318                 'id' => 'abc',
319             ]
320         ]);
321
322         $resp = $this->asAdmin()->post("/import/{$import->id}");
323         $resp->assertRedirect($import->getUrl());
324
325         $resp = $this->followRedirects($resp);
326         $resp->assertSeeText('The name field is required.');
327         $resp->assertSeeText('The id must be an integer.');
328     }
329
330     public function test_run_checks_permissions_on_import()
331     {
332         $viewer = $this->users->viewer();
333         $this->permissions->grantUserRolePermissions($viewer, ['content-import']);
334         $import = ZipTestHelper::importFromData(['created_by' => $viewer->id], [
335             'book' => ['name' => 'My import book'],
336         ]);
337
338         $resp = $this->asViewer()->post("/import/{$import->id}");
339         $resp->assertRedirect($import->getUrl());
340
341         $resp = $this->followRedirects($resp);
342         $resp->assertSeeText('You are lacking the required permissions to create books.');
343     }
344
345     public function test_run_requires_parent_for_chapter_and_page_imports()
346     {
347         $book = $this->entities->book();
348         $pageImport = ZipTestHelper::importFromData([], [
349             'page' => ['name' => 'My page', 'html' => '<p>page test!</p>'],
350         ]);
351         $chapterImport = ZipTestHelper::importFromData([], [
352             'chapter' => ['name' => 'My chapter'],
353         ]);
354
355         $resp = $this->asAdmin()->post("/import/{$pageImport->id}");
356         $resp->assertRedirect($pageImport->getUrl());
357         $this->followRedirects($resp)->assertSee('The parent field is required.');
358
359         $resp = $this->asAdmin()->post("/import/{$pageImport->id}", ['parent' => "book:{$book->id}"]);
360         $resp->assertRedirectContains($book->getUrl());
361
362         $resp = $this->asAdmin()->post("/import/{$chapterImport->id}");
363         $resp->assertRedirect($chapterImport->getUrl());
364         $this->followRedirects($resp)->assertSee('The parent field is required.');
365
366         $resp = $this->asAdmin()->post("/import/{$chapterImport->id}", ['parent' => "book:{$book->id}"]);
367         $resp->assertRedirectContains($book->getUrl());
368     }
369
370     public function test_run_validates_correct_parent_type()
371     {
372         $chapter = $this->entities->chapter();
373         $import = ZipTestHelper::importFromData([], [
374             'chapter' => ['name' => 'My chapter'],
375         ]);
376
377         $resp = $this->asAdmin()->post("/import/{$import->id}", ['parent' => "chapter:{$chapter->id}"]);
378         $resp->assertRedirect($import->getUrl());
379
380         $resp = $this->followRedirects($resp);
381         $resp->assertSee('Parent book required for chapter import.');
382     }
383
384     protected function runImportFromFile(UploadedFile $file): TestResponse
385     {
386         return $this->call('POST', '/import', [], [], ['file' => $file]);
387     }
388 }