diff --git a/app/Exports/Controllers/ImportController.php b/app/Exports/Controllers/ImportController.php index d8dceed2f..a20c341fb 100644 --- a/app/Exports/Controllers/ImportController.php +++ b/app/Exports/Controllers/ImportController.php @@ -70,9 +70,11 @@ class ImportController extends Controller ]); } + /** + * Run the import process against an uploaded import ZIP. + */ public function run(int $id, Request $request) { - // TODO - Test access/visibility $import = $this->imports->findVisible($id); $parent = null; diff --git a/database/factories/Exports/ImportFactory.php b/database/factories/Exports/ImportFactory.php index 74a2bcd65..5d0b4f892 100644 --- a/database/factories/Exports/ImportFactory.php +++ b/database/factories/Exports/ImportFactory.php @@ -21,7 +21,7 @@ class ImportFactory extends Factory public function definition(): array { return [ - 'path' => 'uploads/imports/' . Str::random(10) . '.zip', + 'path' => 'uploads/files/imports/' . Str::random(10) . '.zip', 'name' => $this->faker->words(3, true), 'type' => 'book', 'metadata' => '{"name": "My book"}', diff --git a/tests/Exports/ZipImportRunnerTest.php b/tests/Exports/ZipImportRunnerTest.php new file mode 100644 index 000000000..7bdd8ecbb --- /dev/null +++ b/tests/Exports/ZipImportRunnerTest.php @@ -0,0 +1,21 @@ +runner = app()->make(ZipImportRunner::class); + } + + // TODO - Test full book import + // TODO - Test full chapter import + // TODO - Test full page import +} diff --git a/tests/Exports/ZipImportTest.php b/tests/Exports/ZipImportTest.php index 2b40100aa..3644e9bdc 100644 --- a/tests/Exports/ZipImportTest.php +++ b/tests/Exports/ZipImportTest.php @@ -3,6 +3,7 @@ namespace Tests\Exports; use BookStack\Activity\ActivityType; +use BookStack\Entities\Models\Book; use BookStack\Exports\Import; use BookStack\Exports\ZipExports\Models\ZipExportBook; use BookStack\Exports\ZipExports\Models\ZipExportChapter; @@ -91,7 +92,7 @@ class ZipImportTest extends TestCase public function test_error_shown_if_no_importable_key() { $this->asAdmin(); - $resp = $this->runImportFromFile($this->zipUploadFromData([ + $resp = $this->runImportFromFile(ZipTestHelper::zipUploadFromData([ 'instance' => [] ])); @@ -103,7 +104,7 @@ class ZipImportTest extends TestCase public function test_zip_data_validation_messages_shown() { $this->asAdmin(); - $resp = $this->runImportFromFile($this->zipUploadFromData([ + $resp = $this->runImportFromFile(ZipTestHelper::zipUploadFromData([ 'book' => [ 'id' => 4, 'pages' => [ @@ -154,7 +155,7 @@ class ZipImportTest extends TestCase ], ]; - $resp = $this->runImportFromFile($this->zipUploadFromData($data)); + $resp = $this->runImportFromFile(ZipTestHelper::zipUploadFromData($data)); $this->assertDatabaseHas('imports', [ 'name' => 'My great book name', @@ -217,7 +218,7 @@ class ZipImportTest extends TestCase public function test_import_delete() { $this->asAdmin(); - $this->runImportFromFile($this->zipUploadFromData([ + $this->runImportFromFile(ZipTestHelper::zipUploadFromData([ 'book' => [ 'name' => 'My great book name' ], @@ -262,20 +263,126 @@ class ZipImportTest extends TestCase $this->delete("/import/{$adminImport->id}")->assertRedirect('/import'); } + public function test_run_simple_success_scenario() + { + $import = ZipTestHelper::importFromData([], [ + 'book' => [ + 'name' => 'My imported book', + 'pages' => [ + [ + 'name' => 'My imported book page', + 'html' => '
Hello there from child page!
' + ] + ], + ] + ]); + + $resp = $this->asAdmin()->post("/import/{$import->id}"); + $book = Book::query()->where('name', '=', 'My imported book')->latest()->first(); + $resp->assertRedirect($book->getUrl()); + + $resp = $this->followRedirects($resp); + $resp->assertSee('My imported book page'); + $resp->assertSee('Hello there from child page!'); + + $this->assertDatabaseMissing('imports', ['id' => $import->id]); + $this->assertFileDoesNotExist(storage_path($import->path)); + $this->assertActivityExists(ActivityType::IMPORT_RUN, null, $import->logDescriptor()); + } + + public function test_import_run_access_limited() + { + $user = $this->users->editor(); + $admin = $this->users->admin(); + $userImport = Import::factory()->create(['name' => 'MySuperUserImport', 'created_by' => $user->id]); + $adminImport = Import::factory()->create(['name' => 'MySuperAdminImport', 'created_by' => $admin->id]); + $this->actingAs($user); + + $this->post("/import/{$userImport->id}")->assertRedirect('/'); + $this->post("/import/{$adminImport->id}")->assertRedirect('/'); + + $this->permissions->grantUserRolePermissions($user, ['content-import']); + + $this->post("/import/{$userImport->id}")->assertRedirect($userImport->getUrl()); // Getting validation response instead of access issue response + $this->post("/import/{$adminImport->id}")->assertStatus(404); + + $this->permissions->grantUserRolePermissions($user, ['settings-manage']); + + $this->post("/import/{$adminImport->id}")->assertRedirect($adminImport->getUrl()); // Getting validation response instead of access issue response + } + + public function test_run_revalidates_content() + { + $import = ZipTestHelper::importFromData([], [ + 'book' => [ + 'id' => 'abc', + ] + ]); + + $resp = $this->asAdmin()->post("/import/{$import->id}"); + $resp->assertRedirect($import->getUrl()); + + $resp = $this->followRedirects($resp); + $resp->assertSeeText('The name field is required.'); + $resp->assertSeeText('The id must be an integer.'); + } + + public function test_run_checks_permissions_on_import() + { + $viewer = $this->users->viewer(); + $this->permissions->grantUserRolePermissions($viewer, ['content-import']); + $import = ZipTestHelper::importFromData(['created_by' => $viewer->id], [ + 'book' => ['name' => 'My import book'], + ]); + + $resp = $this->asViewer()->post("/import/{$import->id}"); + $resp->assertRedirect($import->getUrl()); + + $resp = $this->followRedirects($resp); + $resp->assertSeeText('You are lacking the required permissions to create books.'); + } + + public function test_run_requires_parent_for_chapter_and_page_imports() + { + $book = $this->entities->book(); + $pageImport = ZipTestHelper::importFromData([], [ + 'page' => ['name' => 'My page', 'html' => 'page test!
'], + ]); + $chapterImport = ZipTestHelper::importFromData([], [ + 'chapter' => ['name' => 'My chapter'], + ]); + + $resp = $this->asAdmin()->post("/import/{$pageImport->id}"); + $resp->assertRedirect($pageImport->getUrl()); + $this->followRedirects($resp)->assertSee('The parent field is required.'); + + $resp = $this->asAdmin()->post("/import/{$pageImport->id}", ['parent' => "book:{$book->id}"]); + $resp->assertRedirectContains($book->getUrl()); + + $resp = $this->asAdmin()->post("/import/{$chapterImport->id}"); + $resp->assertRedirect($chapterImport->getUrl()); + $this->followRedirects($resp)->assertSee('The parent field is required.'); + + $resp = $this->asAdmin()->post("/import/{$chapterImport->id}", ['parent' => "book:{$book->id}"]); + $resp->assertRedirectContains($book->getUrl()); + } + + public function test_run_validates_correct_parent_type() + { + $chapter = $this->entities->chapter(); + $import = ZipTestHelper::importFromData([], [ + 'chapter' => ['name' => 'My chapter'], + ]); + + $resp = $this->asAdmin()->post("/import/{$import->id}", ['parent' => "chapter:{$chapter->id}"]); + $resp->assertRedirect($import->getUrl()); + + $resp = $this->followRedirects($resp); + $resp->assertSee('Parent book required for chapter import.'); + } + protected function runImportFromFile(UploadedFile $file): TestResponse { return $this->call('POST', '/import', [], [], ['file' => $file]); } - - protected function zipUploadFromData(array $data): UploadedFile - { - $zipFile = tempnam(sys_get_temp_dir(), 'bstest-'); - - $zip = new ZipArchive(); - $zip->open($zipFile, ZipArchive::CREATE); - $zip->addFromString('data.json', json_encode($data)); - $zip->close(); - - return new UploadedFile($zipFile, 'upload.zip', 'application/zip', null, true); - } } diff --git a/tests/Exports/ZipTestHelper.php b/tests/Exports/ZipTestHelper.php new file mode 100644 index 000000000..3a9b34354 --- /dev/null +++ b/tests/Exports/ZipTestHelper.php @@ -0,0 +1,47 @@ +create($importData); + $zip = static::zipUploadFromData($zipData); + rename($zip->getRealPath(), storage_path($import->path)); + + return $import; + } + + public static function deleteZipForImport(Import $import): void + { + $path = storage_path($import->path); + if (file_exists($path)) { + unlink($path); + } + } + + public static function zipUploadFromData(array $data): UploadedFile + { + $zipFile = tempnam(sys_get_temp_dir(), 'bstest-'); + + $zip = new ZipArchive(); + $zip->open($zipFile, ZipArchive::CREATE); + $zip->addFromString('data.json', json_encode($data)); + $zip->close(); + + return new UploadedFile($zipFile, 'upload.zip', 'application/zip', null, true); + } +}