Skip to content

Commit 8f6f819

Browse files
committed
ZIP Imports: Fleshed out continue page, Added testing
1 parent c6109c7 commit 8f6f819

File tree

6 files changed

+206
-6
lines changed

6 files changed

+206
-6
lines changed

app/Exports/Controllers/ImportController.php

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,8 @@ public function __construct(
2323
* Show the view to start a new import, and also list out the existing
2424
* in progress imports that are visible to the user.
2525
*/
26-
public function start(Request $request)
26+
public function start()
2727
{
28-
// TODO - Test visibility access for listed items
2928
$imports = $this->imports->getVisibleImports();
3029

3130
$this->setPageTitle(trans('entities.import'));
@@ -64,7 +63,6 @@ public function upload(Request $request)
6463
*/
6564
public function show(int $id)
6665
{
67-
// TODO - Test visibility access
6866
$import = $this->imports->findVisible($id);
6967

7068
$this->setPageTitle(trans('entities.import_continue'));
@@ -74,12 +72,23 @@ public function show(int $id)
7472
]);
7573
}
7674

75+
public function run(int $id)
76+
{
77+
// TODO - Test access/visibility
78+
79+
$import = $this->imports->findVisible($id);
80+
81+
// TODO - Run import
82+
// Validate again before
83+
// TODO - Redirect to result
84+
// TOOD - Or redirect back with errors
85+
}
86+
7787
/**
7888
* Delete an active pending import from the filesystem and database.
7989
*/
8090
public function delete(int $id)
8191
{
82-
// TODO - Test visibility access
8392
$import = $this->imports->findVisible($id);
8493
$this->imports->deleteImport($import);
8594

app/Exports/Import.php

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,14 @@
33
namespace BookStack\Exports;
44

55
use BookStack\Activity\Models\Loggable;
6+
use BookStack\Users\Models\User;
67
use Carbon\Carbon;
78
use Illuminate\Database\Eloquent\Factories\HasFactory;
89
use Illuminate\Database\Eloquent\Model;
10+
use Illuminate\Database\Eloquent\Relations\BelongsTo;
911

1012
/**
13+
* @property int $id
1114
* @property string $path
1215
* @property string $name
1316
* @property int $size - ZIP size in bytes
@@ -17,6 +20,7 @@
1720
* @property int $created_by
1821
* @property Carbon $created_at
1922
* @property Carbon $updated_at
23+
* @property User $createdBy
2024
*/
2125
class Import extends Model implements Loggable
2226
{
@@ -59,4 +63,9 @@ public function logDescriptor(): string
5963
{
6064
return "({$this->id}) {$this->name}";
6165
}
66+
67+
public function createdBy(): BelongsTo
68+
{
69+
return $this->belongsTo(User::class, 'created_by');
70+
}
6271
}

lang/en/entities.php

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@
5151
'import_pending' => 'Pending Imports',
5252
'import_pending_none' => 'No imports have been started.',
5353
'import_continue' => 'Continue Import',
54+
'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.',
5455
'import_run' => 'Run Import',
56+
'import_size' => 'Import ZIP Size:',
57+
'import_uploaded_at' => 'Uploaded:',
58+
'import_uploaded_by' => 'Uploaded by:',
5559
'import_delete_confirm' => 'Are you sure you want to delete this import?',
5660
'import_delete_desc' => 'This will delete the uploaded import ZIP file, and cannot be undone.',
5761

resources/views/exports/import-show.blade.php

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,44 @@
66

77
<main class="card content-wrap auto-height mt-xxl">
88
<h1 class="list-heading">{{ trans('entities.import_continue') }}</h1>
9-
<form action="{{ url('/import') }}" enctype="multipart/form-data" method="POST">
9+
<p class="text-muted">{{ trans('entities.import_continue_desc') }}</p>
10+
11+
<div class="mb-m">
12+
@php
13+
$type = $import->getType();
14+
@endphp
15+
<div class="flex-container-row items-center justify-space-between wrap">
16+
<div class="py-s">
17+
<p class="text-{{ $type }} mb-xs bold">@icon($type) {{ $import->name }}</p>
18+
@if($type === 'book')
19+
<p class="text-chapter mb-xs ml-l">@icon('chapter') {{ trans_choice('entities.x_chapters', $import->chapter_count) }}</p>
20+
@endif
21+
@if($type === 'book' || $type === 'chapter')
22+
<p class="text-page mb-xs ml-l">@icon('page') {{ trans_choice('entities.x_pages', $import->page_count) }}</p>
23+
@endif
24+
</div>
25+
<div class="py-s">
26+
<div class="opacity-80">
27+
<strong>{{ trans('entities.import_size') }}</strong>
28+
<span>{{ $import->getSizeString() }}</span>
29+
</div>
30+
<div class="opacity-80">
31+
<strong>{{ trans('entities.import_uploaded_at') }}</strong>
32+
<span title="{{ $import->created_at->toISOString() }}">{{ $import->created_at->diffForHumans() }}</span>
33+
</div>
34+
@if($import->createdBy)
35+
<div class="opacity-80">
36+
<strong>{{ trans('entities.import_uploaded_by') }}</strong>
37+
<a href="{{ $import->createdBy->getProfileUrl() }}">{{ $import->createdBy->name }}</a>
38+
</div>
39+
@endif
40+
</div>
41+
</div>
42+
</div>
43+
44+
<form id="import-run-form"
45+
action="{{ $import->getUrl() }}"
46+
method="POST">
1047
{{ csrf_field() }}
1148
</form>
1249

@@ -23,7 +60,7 @@ class="button outline">{{ trans('common.delete') }}</button>
2360
<button type="submit" form="import-delete-form" class="text-link small text-item">{{ trans('common.confirm') }}</button>
2461
</div>
2562
</div>
26-
<button type="submit" class="button">{{ trans('entities.import_run') }}</button>
63+
<button type="submit" form="import-run-form" class="button">{{ trans('entities.import_run') }}</button>
2764
</div>
2865
</main>
2966
</div>

routes/web.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,7 @@
210210
Route::get('/import', [ExportControllers\ImportController::class, 'start']);
211211
Route::post('/import', [ExportControllers\ImportController::class, 'upload']);
212212
Route::get('/import/{id}', [ExportControllers\ImportController::class, 'show']);
213+
Route::post('/import/{id}', [ExportControllers\ImportController::class, 'run']);
213214
Route::delete('/import/{id}', [ExportControllers\ImportController::class, 'delete']);
214215

215216
// Other Pages

tests/Exports/ZipImportTest.php

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace Tests\Exports;
44

5+
use BookStack\Activity\ActivityType;
6+
use BookStack\Exports\Import;
57
use Illuminate\Http\UploadedFile;
68
use Illuminate\Testing\TestResponse;
79
use Tests\TestCase;
@@ -35,6 +37,25 @@ public function test_permissions_needed_for_import_page()
3537
$resp->assertSeeText('Select ZIP file to upload');
3638
}
3739

40+
public function test_import_page_pending_import_visibility_limited()
41+
{
42+
$user = $this->users->viewer();
43+
$admin = $this->users->admin();
44+
$userImport = Import::factory()->create(['name' => 'MySuperUserImport', 'created_by' => $user->id]);
45+
$adminImport = Import::factory()->create(['name' => 'MySuperAdminImport', 'created_by' => $admin->id]);
46+
$this->permissions->grantUserRolePermissions($user, ['content-import']);
47+
48+
$resp = $this->actingAs($user)->get('/import');
49+
$resp->assertSeeText('MySuperUserImport');
50+
$resp->assertDontSeeText('MySuperAdminImport');
51+
52+
$this->permissions->grantUserRolePermissions($user, ['settings-manage']);
53+
54+
$resp = $this->actingAs($user)->get('/import');
55+
$resp->assertSeeText('MySuperUserImport');
56+
$resp->assertSeeText('MySuperAdminImport');
57+
}
58+
3859
public function test_zip_read_errors_are_shown_on_validation()
3960
{
4061
$invalidUpload = $this->files->uploadedImage('image.zip');
@@ -105,6 +126,125 @@ public function test_zip_data_validation_messages_shown()
105126
$resp->assertSeeText('[book.pages.1.tags.0.value]: The value must be a string.');
106127
}
107128

129+
public function test_import_upload_success()
130+
{
131+
$admin = $this->users->admin();
132+
$this->actingAs($admin);
133+
$resp = $this->runImportFromFile($this->zipUploadFromData([
134+
'book' => [
135+
'name' => 'My great book name',
136+
'chapters' => [
137+
[
138+
'name' => 'my chapter',
139+
'pages' => [
140+
[
141+
'name' => 'my chapter page',
142+
]
143+
]
144+
]
145+
],
146+
'pages' => [
147+
[
148+
'name' => 'My page',
149+
]
150+
],
151+
],
152+
]));
153+
154+
$this->assertDatabaseHas('imports', [
155+
'name' => 'My great book name',
156+
'book_count' => 1,
157+
'chapter_count' => 1,
158+
'page_count' => 2,
159+
'created_by' => $admin->id,
160+
]);
161+
162+
/** @var Import $import */
163+
$import = Import::query()->latest()->first();
164+
$resp->assertRedirect("/import/{$import->id}");
165+
$this->assertFileExists(storage_path($import->path));
166+
$this->assertActivityExists(ActivityType::IMPORT_CREATE);
167+
}
168+
169+
public function test_import_show_page()
170+
{
171+
$import = Import::factory()->create(['name' => 'MySuperAdminImport']);
172+
173+
$resp = $this->asAdmin()->get("/import/{$import->id}");
174+
$resp->assertOk();
175+
$resp->assertSee('MySuperAdminImport');
176+
}
177+
178+
public function test_import_show_page_access_limited()
179+
{
180+
$user = $this->users->viewer();
181+
$admin = $this->users->admin();
182+
$userImport = Import::factory()->create(['name' => 'MySuperUserImport', 'created_by' => $user->id]);
183+
$adminImport = Import::factory()->create(['name' => 'MySuperAdminImport', 'created_by' => $admin->id]);
184+
$this->actingAs($user);
185+
186+
$this->get("/import/{$userImport->id}")->assertRedirect('/');
187+
$this->get("/import/{$adminImport->id}")->assertRedirect('/');
188+
189+
$this->permissions->grantUserRolePermissions($user, ['content-import']);
190+
191+
$this->get("/import/{$userImport->id}")->assertOk();
192+
$this->get("/import/{$adminImport->id}")->assertStatus(404);
193+
194+
$this->permissions->grantUserRolePermissions($user, ['settings-manage']);
195+
196+
$this->get("/import/{$userImport->id}")->assertOk();
197+
$this->get("/import/{$adminImport->id}")->assertOk();
198+
}
199+
200+
public function test_import_delete()
201+
{
202+
$this->asAdmin();
203+
$this->runImportFromFile($this->zipUploadFromData([
204+
'book' => [
205+
'name' => 'My great book name'
206+
],
207+
]));
208+
209+
/** @var Import $import */
210+
$import = Import::query()->latest()->first();
211+
$this->assertDatabaseHas('imports', [
212+
'id' => $import->id,
213+
'name' => 'My great book name'
214+
]);
215+
$this->assertFileExists(storage_path($import->path));
216+
217+
$resp = $this->delete("/import/{$import->id}");
218+
219+
$resp->assertRedirect('/import');
220+
$this->assertActivityExists(ActivityType::IMPORT_DELETE);
221+
$this->assertDatabaseMissing('imports', [
222+
'id' => $import->id,
223+
]);
224+
$this->assertFileDoesNotExist(storage_path($import->path));
225+
}
226+
227+
public function test_import_delete_access_limited()
228+
{
229+
$user = $this->users->viewer();
230+
$admin = $this->users->admin();
231+
$userImport = Import::factory()->create(['name' => 'MySuperUserImport', 'created_by' => $user->id]);
232+
$adminImport = Import::factory()->create(['name' => 'MySuperAdminImport', 'created_by' => $admin->id]);
233+
$this->actingAs($user);
234+
235+
$this->delete("/import/{$userImport->id}")->assertRedirect('/');
236+
$this->delete("/import/{$adminImport->id}")->assertRedirect('/');
237+
238+
$this->permissions->grantUserRolePermissions($user, ['content-import']);
239+
240+
$this->delete("/import/{$userImport->id}")->assertRedirect('/import');
241+
$this->delete("/import/{$adminImport->id}")->assertStatus(404);
242+
243+
$this->permissions->grantUserRolePermissions($user, ['settings-manage']);
244+
245+
$this->delete("/import/{$adminImport->id}")->assertRedirect('/import');
246+
}
247+
108248
protected function runImportFromFile(UploadedFile $file): TestResponse
109249
{
110250
return $this->call('POST', '/import', [], [], ['file' => $file]);

0 commit comments

Comments
 (0)