3 namespace Tests\Exports;
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;
16 class ZipImportTest extends TestCase
18 public function test_import_page_view()
20 $resp = $this->asAdmin()->get('/import');
21 $resp->assertSee('Import');
22 $this->withHtml($resp)->assertElementExists('form input[type="file"][name="file"]');
25 public function test_permissions_needed_for_import_page()
27 $user = $this->users->viewer();
28 $this->actingAs($user);
30 $resp = $this->get('/books');
31 $this->withHtml($resp)->assertLinkNotExists(url('/import'));
32 $resp = $this->get('/import');
33 $resp->assertRedirect('/');
35 $this->permissions->grantUserRolePermissions($user, ['content-import']);
37 $resp = $this->get('/books');
38 $this->withHtml($resp)->assertLinkExists(url('/import'));
39 $resp = $this->get('/import');
41 $resp->assertSeeText('Select ZIP file to upload');
44 public function test_import_page_pending_import_visibility_limited()
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']);
52 $resp = $this->actingAs($user)->get('/import');
53 $resp->assertSeeText('MySuperUserImport');
54 $resp->assertDontSeeText('MySuperAdminImport');
56 $this->permissions->grantUserRolePermissions($user, ['settings-manage']);
58 $resp = $this->actingAs($user)->get('/import');
59 $resp->assertSeeText('MySuperUserImport');
60 $resp->assertSeeText('MySuperAdminImport');
63 public function test_zip_read_errors_are_shown_on_validation()
65 $invalidUpload = $this->files->uploadedImage('image.zip');
68 $resp = $this->runImportFromFile($invalidUpload);
69 $resp->assertRedirect('/import');
71 $resp = $this->followRedirects($resp);
72 $resp->assertSeeText('Could not read ZIP file');
75 public function test_error_shown_if_missing_data()
77 $zipFile = tempnam(sys_get_temp_dir(), 'bstest-');
78 $zip = new ZipArchive();
79 $zip->open($zipFile, ZipArchive::CREATE);
80 $zip->addFromString('beans', 'cat');
84 $upload = new UploadedFile($zipFile, 'upload.zip', 'application/zip', null, true);
85 $resp = $this->runImportFromFile($upload);
86 $resp->assertRedirect('/import');
88 $resp = $this->followRedirects($resp);
89 $resp->assertSeeText('Could not find and decode ZIP data.json content.');
92 public function test_error_shown_if_no_importable_key()
95 $resp = $this->runImportFromFile(ZipTestHelper::zipUploadFromData([
99 $resp->assertRedirect('/import');
100 $resp = $this->followRedirects($resp);
101 $resp->assertSeeText('ZIP file data has no expected book, chapter or page content.');
104 public function test_zip_data_validation_messages_shown()
107 $resp = $this->runImportFromFile(ZipTestHelper::zipUploadFromData([
113 'name' => 'My inner page',
124 $resp->assertRedirect('/import');
125 $resp = $this->followRedirects($resp);
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.');
133 public function test_import_upload_success()
135 $admin = $this->users->admin();
136 $this->actingAs($admin);
139 'name' => 'My great book name',
142 'name' => 'my chapter',
145 'name' => 'my chapter page',
158 $resp = $this->runImportFromFile(ZipTestHelper::zipUploadFromData($data));
160 $this->assertDatabaseHas('imports', [
161 'name' => 'My great book name',
163 'created_by' => $admin->id,
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);
172 ZipTestHelper::deleteZipForImport($import);
175 public function test_import_show_page()
177 $exportBook = new ZipExportBook();
178 $exportBook->name = 'My exported book';
179 $exportChapter = new ZipExportChapter();
180 $exportChapter->name = 'My exported chapter';
181 $exportPage = new ZipExportPage();
182 $exportPage->name = 'My exported page';
183 $exportBook->chapters = [$exportChapter];
184 $exportChapter->pages = [$exportPage];
186 $import = Import::factory()->create([
187 'name' => 'MySuperAdminImport',
188 'metadata' => json_encode($exportBook)
191 $resp = $this->asAdmin()->get("/import/{$import->id}");
193 $resp->assertSeeText('My exported book');
194 $resp->assertSeeText('My exported chapter');
195 $resp->assertSeeText('My exported page');
198 public function test_import_show_page_access_limited()
200 $user = $this->users->viewer();
201 $admin = $this->users->admin();
202 $userImport = Import::factory()->create(['name' => 'MySuperUserImport', 'created_by' => $user->id]);
203 $adminImport = Import::factory()->create(['name' => 'MySuperAdminImport', 'created_by' => $admin->id]);
204 $this->actingAs($user);
206 $this->get("/import/{$userImport->id}")->assertRedirect('/');
207 $this->get("/import/{$adminImport->id}")->assertRedirect('/');
209 $this->permissions->grantUserRolePermissions($user, ['content-import']);
211 $this->get("/import/{$userImport->id}")->assertOk();
212 $this->get("/import/{$adminImport->id}")->assertStatus(404);
214 $this->permissions->grantUserRolePermissions($user, ['settings-manage']);
216 $this->get("/import/{$userImport->id}")->assertOk();
217 $this->get("/import/{$adminImport->id}")->assertOk();
220 public function test_import_delete()
223 $this->runImportFromFile(ZipTestHelper::zipUploadFromData([
225 'name' => 'My great book name'
229 /** @var Import $import */
230 $import = Import::query()->latest()->first();
231 $this->assertDatabaseHas('imports', [
233 'name' => 'My great book name'
235 $this->assertFileExists(storage_path($import->path));
237 $resp = $this->delete("/import/{$import->id}");
239 $resp->assertRedirect('/import');
240 $this->assertActivityExists(ActivityType::IMPORT_DELETE);
241 $this->assertDatabaseMissing('imports', [
244 $this->assertFileDoesNotExist(storage_path($import->path));
247 public function test_import_delete_access_limited()
249 $user = $this->users->viewer();
250 $admin = $this->users->admin();
251 $userImport = Import::factory()->create(['name' => 'MySuperUserImport', 'created_by' => $user->id]);
252 $adminImport = Import::factory()->create(['name' => 'MySuperAdminImport', 'created_by' => $admin->id]);
253 $this->actingAs($user);
255 $this->delete("/import/{$userImport->id}")->assertRedirect('/');
256 $this->delete("/import/{$adminImport->id}")->assertRedirect('/');
258 $this->permissions->grantUserRolePermissions($user, ['content-import']);
260 $this->delete("/import/{$userImport->id}")->assertRedirect('/import');
261 $this->delete("/import/{$adminImport->id}")->assertStatus(404);
263 $this->permissions->grantUserRolePermissions($user, ['settings-manage']);
265 $this->delete("/import/{$adminImport->id}")->assertRedirect('/import');
268 public function test_run_simple_success_scenario()
270 $import = ZipTestHelper::importFromData([], [
272 'name' => 'My imported book',
275 'name' => 'My imported book page',
276 'html' => '<p>Hello there from child page!</p>'
282 $resp = $this->asAdmin()->post("/import/{$import->id}");
283 $book = Book::query()->where('name', '=', 'My imported book')->latest()->first();
284 $resp->assertRedirect($book->getUrl());
286 $resp = $this->followRedirects($resp);
287 $resp->assertSee('My imported book page');
288 $resp->assertSee('Hello there from child page!');
290 $this->assertDatabaseMissing('imports', ['id' => $import->id]);
291 $this->assertFileDoesNotExist(storage_path($import->path));
292 $this->assertActivityExists(ActivityType::IMPORT_RUN, null, $import->logDescriptor());
295 public function test_import_run_access_limited()
297 $user = $this->users->editor();
298 $admin = $this->users->admin();
299 $userImport = Import::factory()->create(['name' => 'MySuperUserImport', 'created_by' => $user->id]);
300 $adminImport = Import::factory()->create(['name' => 'MySuperAdminImport', 'created_by' => $admin->id]);
301 $this->actingAs($user);
303 $this->post("/import/{$userImport->id}")->assertRedirect('/');
304 $this->post("/import/{$adminImport->id}")->assertRedirect('/');
306 $this->permissions->grantUserRolePermissions($user, ['content-import']);
308 $this->post("/import/{$userImport->id}")->assertRedirect($userImport->getUrl()); // Getting validation response instead of access issue response
309 $this->post("/import/{$adminImport->id}")->assertStatus(404);
311 $this->permissions->grantUserRolePermissions($user, ['settings-manage']);
313 $this->post("/import/{$adminImport->id}")->assertRedirect($adminImport->getUrl()); // Getting validation response instead of access issue response
316 public function test_run_revalidates_content()
318 $import = ZipTestHelper::importFromData([], [
324 $resp = $this->asAdmin()->post("/import/{$import->id}");
325 $resp->assertRedirect($import->getUrl());
327 $resp = $this->followRedirects($resp);
328 $resp->assertSeeText('The name field is required.');
329 $resp->assertSeeText('The id must be an integer.');
331 ZipTestHelper::deleteZipForImport($import);
334 public function test_run_checks_permissions_on_import()
336 $viewer = $this->users->viewer();
337 $this->permissions->grantUserRolePermissions($viewer, ['content-import']);
338 $import = ZipTestHelper::importFromData(['created_by' => $viewer->id], [
339 'book' => ['name' => 'My import book'],
342 $resp = $this->asViewer()->post("/import/{$import->id}");
343 $resp->assertRedirect($import->getUrl());
345 $resp = $this->followRedirects($resp);
346 $resp->assertSeeText('You are lacking the required permissions to create books.');
348 ZipTestHelper::deleteZipForImport($import);
351 public function test_run_requires_parent_for_chapter_and_page_imports()
353 $book = $this->entities->book();
354 $pageImport = ZipTestHelper::importFromData([], [
355 'page' => ['name' => 'My page', 'html' => '<p>page test!</p>'],
357 $chapterImport = ZipTestHelper::importFromData([], [
358 'chapter' => ['name' => 'My chapter'],
361 $resp = $this->asAdmin()->post("/import/{$pageImport->id}");
362 $resp->assertRedirect($pageImport->getUrl());
363 $this->followRedirects($resp)->assertSee('The parent field is required.');
365 $resp = $this->asAdmin()->post("/import/{$pageImport->id}", ['parent' => "book:{$book->id}"]);
366 $resp->assertRedirectContains($book->getUrl());
368 $resp = $this->asAdmin()->post("/import/{$chapterImport->id}");
369 $resp->assertRedirect($chapterImport->getUrl());
370 $this->followRedirects($resp)->assertSee('The parent field is required.');
372 $resp = $this->asAdmin()->post("/import/{$chapterImport->id}", ['parent' => "book:{$book->id}"]);
373 $resp->assertRedirectContains($book->getUrl());
376 public function test_run_validates_correct_parent_type()
378 $chapter = $this->entities->chapter();
379 $import = ZipTestHelper::importFromData([], [
380 'chapter' => ['name' => 'My chapter'],
383 $resp = $this->asAdmin()->post("/import/{$import->id}", ['parent' => "chapter:{$chapter->id}"]);
384 $resp->assertRedirect($import->getUrl());
386 $resp = $this->followRedirects($resp);
387 $resp->assertSee('Parent book required for chapter import.');
389 ZipTestHelper::deleteZipForImport($import);
392 protected function runImportFromFile(UploadedFile $file): TestResponse
394 return $this->call('POST', '/import', [], [], ['file' => $file]);