ZIP Imports: Fleshed out continue page, Added testing
Some checks are pending
analyse-php / build (push) Waiting to run
lint-php / build (push) Waiting to run
test-migrations / build (8.1) (push) Waiting to run
test-migrations / build (8.2) (push) Waiting to run
test-migrations / build (8.3) (push) Waiting to run
test-php / build (8.2) (push) Waiting to run
test-php / build (8.3) (push) Waiting to run
test-php / build (8.1) (push) Waiting to run

This commit is contained in:
Dan Brown 2024-11-03 17:28:18 +00:00
parent c6109c7087
commit 8f6f81948e
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
6 changed files with 206 additions and 6 deletions

View File

@ -23,9 +23,8 @@ class ImportController extends Controller
* Show the view to start a new import, and also list out the existing
* in progress imports that are visible to the user.
*/
public function start(Request $request)
public function start()
{
// TODO - Test visibility access for listed items
$imports = $this->imports->getVisibleImports();
$this->setPageTitle(trans('entities.import'));
@ -64,7 +63,6 @@ class ImportController extends Controller
*/
public function show(int $id)
{
// TODO - Test visibility access
$import = $this->imports->findVisible($id);
$this->setPageTitle(trans('entities.import_continue'));
@ -74,12 +72,23 @@ class ImportController extends Controller
]);
}
public function run(int $id)
{
// TODO - Test access/visibility
$import = $this->imports->findVisible($id);
// TODO - Run import
// Validate again before
// TODO - Redirect to result
// TOOD - Or redirect back with errors
}
/**
* Delete an active pending import from the filesystem and database.
*/
public function delete(int $id)
{
// TODO - Test visibility access
$import = $this->imports->findVisible($id);
$this->imports->deleteImport($import);

View File

@ -3,11 +3,14 @@
namespace BookStack\Exports;
use BookStack\Activity\Models\Loggable;
use BookStack\Users\Models\User;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
/**
* @property int $id
* @property string $path
* @property string $name
* @property int $size - ZIP size in bytes
@ -17,6 +20,7 @@ use Illuminate\Database\Eloquent\Model;
* @property int $created_by
* @property Carbon $created_at
* @property Carbon $updated_at
* @property User $createdBy
*/
class Import extends Model implements Loggable
{
@ -59,4 +63,9 @@ class Import extends Model implements Loggable
{
return "({$this->id}) {$this->name}";
}
public function createdBy(): BelongsTo
{
return $this->belongsTo(User::class, 'created_by');
}
}

View File

@ -51,7 +51,11 @@ return [
'import_pending' => 'Pending Imports',
'import_pending_none' => 'No imports have been started.',
'import_continue' => 'Continue Import',
'import_continue_desc' => 'Review the content due to be imported from the uploaded ZIP file. When ready, run the import to add its contents to this system. The uploaded ZIP import file will be automatically removed on successful import.',
'import_run' => 'Run Import',
'import_size' => 'Import ZIP Size:',
'import_uploaded_at' => 'Uploaded:',
'import_uploaded_by' => 'Uploaded by:',
'import_delete_confirm' => 'Are you sure you want to delete this import?',
'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',

View File

@ -6,7 +6,44 @@
<main class="card content-wrap auto-height mt-xxl">
<h1 class="list-heading">{{ trans('entities.import_continue') }}</h1>
<form action="{{ url('/import') }}" enctype="multipart/form-data" method="POST">
<p class="text-muted">{{ trans('entities.import_continue_desc') }}</p>
<div class="mb-m">
@php
$type = $import->getType();
@endphp
<div class="flex-container-row items-center justify-space-between wrap">
<div class="py-s">
<p class="text-{{ $type }} mb-xs bold">@icon($type) {{ $import->name }}</p>
@if($type === 'book')
<p class="text-chapter mb-xs ml-l">@icon('chapter') {{ trans_choice('entities.x_chapters', $import->chapter_count) }}</p>
@endif
@if($type === 'book' || $type === 'chapter')
<p class="text-page mb-xs ml-l">@icon('page') {{ trans_choice('entities.x_pages', $import->page_count) }}</p>
@endif
</div>
<div class="py-s">
<div class="opacity-80">
<strong>{{ trans('entities.import_size') }}</strong>
<span>{{ $import->getSizeString() }}</span>
</div>
<div class="opacity-80">
<strong>{{ trans('entities.import_uploaded_at') }}</strong>
<span title="{{ $import->created_at->toISOString() }}">{{ $import->created_at->diffForHumans() }}</span>
</div>
@if($import->createdBy)
<div class="opacity-80">
<strong>{{ trans('entities.import_uploaded_by') }}</strong>
<a href="{{ $import->createdBy->getProfileUrl() }}">{{ $import->createdBy->name }}</a>
</div>
@endif
</div>
</div>
</div>
<form id="import-run-form"
action="{{ $import->getUrl() }}"
method="POST">
{{ csrf_field() }}
</form>
@ -23,7 +60,7 @@
<button type="submit" form="import-delete-form" class="text-link small text-item">{{ trans('common.confirm') }}</button>
</div>
</div>
<button type="submit" class="button">{{ trans('entities.import_run') }}</button>
<button type="submit" form="import-run-form" class="button">{{ trans('entities.import_run') }}</button>
</div>
</main>
</div>

View File

@ -210,6 +210,7 @@ Route::middleware('auth')->group(function () {
Route::get('/import', [ExportControllers\ImportController::class, 'start']);
Route::post('/import', [ExportControllers\ImportController::class, 'upload']);
Route::get('/import/{id}', [ExportControllers\ImportController::class, 'show']);
Route::post('/import/{id}', [ExportControllers\ImportController::class, 'run']);
Route::delete('/import/{id}', [ExportControllers\ImportController::class, 'delete']);
// Other Pages

View File

@ -2,6 +2,8 @@
namespace Tests\Exports;
use BookStack\Activity\ActivityType;
use BookStack\Exports\Import;
use Illuminate\Http\UploadedFile;
use Illuminate\Testing\TestResponse;
use Tests\TestCase;
@ -35,6 +37,25 @@ class ZipImportTest extends TestCase
$resp->assertSeeText('Select ZIP file to upload');
}
public function test_import_page_pending_import_visibility_limited()
{
$user = $this->users->viewer();
$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->permissions->grantUserRolePermissions($user, ['content-import']);
$resp = $this->actingAs($user)->get('/import');
$resp->assertSeeText('MySuperUserImport');
$resp->assertDontSeeText('MySuperAdminImport');
$this->permissions->grantUserRolePermissions($user, ['settings-manage']);
$resp = $this->actingAs($user)->get('/import');
$resp->assertSeeText('MySuperUserImport');
$resp->assertSeeText('MySuperAdminImport');
}
public function test_zip_read_errors_are_shown_on_validation()
{
$invalidUpload = $this->files->uploadedImage('image.zip');
@ -105,6 +126,125 @@ class ZipImportTest extends TestCase
$resp->assertSeeText('[book.pages.1.tags.0.value]: The value must be a string.');
}
public function test_import_upload_success()
{
$admin = $this->users->admin();
$this->actingAs($admin);
$resp = $this->runImportFromFile($this->zipUploadFromData([
'book' => [
'name' => 'My great book name',
'chapters' => [
[
'name' => 'my chapter',
'pages' => [
[
'name' => 'my chapter page',
]
]
]
],
'pages' => [
[
'name' => 'My page',
]
],
],
]));
$this->assertDatabaseHas('imports', [
'name' => 'My great book name',
'book_count' => 1,
'chapter_count' => 1,
'page_count' => 2,
'created_by' => $admin->id,
]);
/** @var Import $import */
$import = Import::query()->latest()->first();
$resp->assertRedirect("/import/{$import->id}");
$this->assertFileExists(storage_path($import->path));
$this->assertActivityExists(ActivityType::IMPORT_CREATE);
}
public function test_import_show_page()
{
$import = Import::factory()->create(['name' => 'MySuperAdminImport']);
$resp = $this->asAdmin()->get("/import/{$import->id}");
$resp->assertOk();
$resp->assertSee('MySuperAdminImport');
}
public function test_import_show_page_access_limited()
{
$user = $this->users->viewer();
$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->get("/import/{$userImport->id}")->assertRedirect('/');
$this->get("/import/{$adminImport->id}")->assertRedirect('/');
$this->permissions->grantUserRolePermissions($user, ['content-import']);
$this->get("/import/{$userImport->id}")->assertOk();
$this->get("/import/{$adminImport->id}")->assertStatus(404);
$this->permissions->grantUserRolePermissions($user, ['settings-manage']);
$this->get("/import/{$userImport->id}")->assertOk();
$this->get("/import/{$adminImport->id}")->assertOk();
}
public function test_import_delete()
{
$this->asAdmin();
$this->runImportFromFile($this->zipUploadFromData([
'book' => [
'name' => 'My great book name'
],
]));
/** @var Import $import */
$import = Import::query()->latest()->first();
$this->assertDatabaseHas('imports', [
'id' => $import->id,
'name' => 'My great book name'
]);
$this->assertFileExists(storage_path($import->path));
$resp = $this->delete("/import/{$import->id}");
$resp->assertRedirect('/import');
$this->assertActivityExists(ActivityType::IMPORT_DELETE);
$this->assertDatabaseMissing('imports', [
'id' => $import->id,
]);
$this->assertFileDoesNotExist(storage_path($import->path));
}
public function test_import_delete_access_limited()
{
$user = $this->users->viewer();
$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->delete("/import/{$userImport->id}")->assertRedirect('/');
$this->delete("/import/{$adminImport->id}")->assertRedirect('/');
$this->permissions->grantUserRolePermissions($user, ['content-import']);
$this->delete("/import/{$userImport->id}")->assertRedirect('/import');
$this->delete("/import/{$adminImport->id}")->assertStatus(404);
$this->permissions->grantUserRolePermissions($user, ['settings-manage']);
$this->delete("/import/{$adminImport->id}")->assertRedirect('/import');
}
protected function runImportFromFile(UploadedFile $file): TestResponse
{
return $this->call('POST', '/import', [], [], ['file' => $file]);