Skip to content

Commit 259aa82

Browse files
committed
ZIP Imports: Added validation message display, added testing
Testing covers main UI access, and main non-successfull import actions. Started planning stored import model. Extracted some text to language files.
1 parent c4ec50d commit 259aa82

File tree

7 files changed

+164
-16
lines changed

7 files changed

+164
-16
lines changed

app/Exports/Controllers/ImportController.php

Lines changed: 15 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@ public function start(Request $request)
1717
{
1818
// TODO - Show existing imports for user (or for all users if admin-level user)
1919

20-
return view('exports.import');
20+
return view('exports.import', [
21+
'zipErrors' => session()->pull('validation_errors') ?? [],
22+
]);
2123
}
2224

2325
public function upload(Request $request)
@@ -31,13 +33,21 @@ public function upload(Request $request)
3133

3234
$errors = (new ZipExportValidator($zipPath))->validate();
3335
if ($errors) {
34-
dd($errors);
36+
session()->flash('validation_errors', $errors);
37+
return redirect('/import');
3538
}
39+
3640
dd('passed');
37-
// TODO - Read existing ZIP upload and send through validator
38-
// TODO - If invalid, return user with errors
3941
// TODO - Upload to storage
40-
// TODO - Store info/results from validator
42+
// TODO - Store info/results for display:
43+
// - zip_path
44+
// - name (From name of thing being imported)
45+
// - size
46+
// - book_count
47+
// - chapter_count
48+
// - page_count
49+
// - created_by
50+
// - created_at/updated_at
4151
// TODO - Send user to next import stage
4252
}
4353
}

app/Exports/ZipExports/ZipExportValidator.php

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,21 +18,21 @@ public function validate(): array
1818
{
1919
// Validate file exists
2020
if (!file_exists($this->zipPath) || !is_readable($this->zipPath)) {
21-
return ['format' => "Could not read ZIP file"];
21+
return ['format' => trans('errors.import_zip_cant_read')];
2222
}
2323

2424
// Validate file is valid zip
2525
$zip = new \ZipArchive();
2626
$opened = $zip->open($this->zipPath, ZipArchive::RDONLY);
2727
if ($opened !== true) {
28-
return ['format' => "Could not read ZIP file"];
28+
return ['format' => trans('errors.import_zip_cant_read')];
2929
}
3030

3131
// Validate json data exists, including metadata
3232
$jsonData = $zip->getFromName('data.json') ?: '';
3333
$importData = json_decode($jsonData, true);
3434
if (!$importData) {
35-
return ['format' => "Could not find and decode ZIP data.json content"];
35+
return ['format' => trans('errors.import_zip_cant_decode_data')];
3636
}
3737

3838
$helper = new ZipValidationHelper($zip);
@@ -47,9 +47,10 @@ public function validate(): array
4747
$modelErrors = ZipExportPage::validate($helper, $importData['page']);
4848
$keyPrefix = 'page';
4949
} else {
50-
return ['format' => "ZIP file has no book, chapter or page data"];
50+
return ['format' => trans('errors.import_zip_no_data')];
5151
}
5252

53+
5354
return $this->flattenModelErrors($modelErrors, $keyPrefix);
5455
}
5556

lang/en/entities.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,9 @@
4545
'default_template_select' => 'Select a template page',
4646
'import' => 'Import',
4747
'import_validate' => 'Validate Import',
48+
'import_desc' => 'Import books, chapters & pages using a portable zip export from the same, or a different, instance. Select a ZIP file to import then press "Validate Import" to proceed. After the file has been uploaded and validated you\'ll be able to configure & confirm the import in the next view.',
49+
'import_zip_select' => 'Select ZIP file to upload',
50+
'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',
4851

4952
// Permissions and restrictions
5053
'permissions' => 'Permissions',

lang/en/errors.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,11 @@
105105
'app_down' => ':appName is down right now',
106106
'back_soon' => 'It will be back up soon.',
107107

108+
// Import
109+
'import_zip_cant_read' => 'Could not read ZIP file.',
110+
'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',
111+
'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',
112+
108113
// API errors
109114
'api_no_authorization_found' => 'No authorization token found on the request',
110115
'api_bad_authorization_format' => 'An authorization token was found on the request but the format appeared incorrect',

lang/en/validation.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@
106106
'uploaded' => 'The file could not be uploaded. The server may not accept files of this size.',
107107

108108
'zip_file' => 'The :attribute needs to reference a file within the ZIP.',
109-
'zip_model_expected' => 'Data object expected but ":type" found',
109+
'zip_model_expected' => 'Data object expected but ":type" found.',
110110

111111
// Custom validation lines
112112
'custom' => [

resources/views/exports/import.blade.php

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,10 @@
99
<form action="{{ url('/import') }}" enctype="multipart/form-data" method="POST">
1010
{{ csrf_field() }}
1111
<div class="flex-container-row justify-space-between wrap gap-x-xl gap-y-s">
12-
<p class="flex min-width-l text-muted mb-s">
13-
Import books, chapters & pages using a portable zip export from the same, or a different, instance.
14-
Select a ZIP file to import then press "Validate Import" to proceed.
15-
After the file has been uploaded and validated you'll be able to configure & confirm the import in the next view.
16-
</p>
12+
<p class="flex min-width-l text-muted mb-s">{{ trans('entities.import_desc') }}</p>
1713
<div class="flex-none min-width-l flex-container-row justify-flex-end">
1814
<div class="mb-m">
19-
<label for="file">Select ZIP file to upload</label>
15+
<label for="file">{{ trans('entities.import_zip_select') }}</label>
2016
<input type="file"
2117
accept=".zip,application/zip,application/x-zip-compressed"
2218
name="file"
@@ -27,6 +23,15 @@ class="custom-simple-file-input">
2723
</div>
2824
</div>
2925

26+
@if(count($zipErrors) > 0)
27+
<p class="mb-xs"><strong class="text-neg">{{ trans('entities.import_zip_validation_errors') }}</strong></p>
28+
<ul class="mb-m">
29+
@foreach($zipErrors as $key => $error)
30+
<li><strong class="text-neg">[{{ $key }}]</strong>: {{ $error }}</li>
31+
@endforeach
32+
</ul>
33+
@endif
34+
3035
<div class="text-right">
3136
<a href="{{ url('/books') }}" class="button outline">{{ trans('common.cancel') }}</a>
3237
<button type="submit" class="button">{{ trans('entities.import_validate') }}</button>

tests/Exports/ZipImportTest.php

Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
<?php
2+
3+
namespace Tests\Exports;
4+
5+
use Illuminate\Http\UploadedFile;
6+
use Illuminate\Testing\TestResponse;
7+
use Tests\TestCase;
8+
use ZipArchive;
9+
10+
class ZipImportTest extends TestCase
11+
{
12+
public function test_import_page_view()
13+
{
14+
$resp = $this->asAdmin()->get('/import');
15+
$resp->assertSee('Import');
16+
$this->withHtml($resp)->assertElementExists('form input[type="file"][name="file"]');
17+
}
18+
19+
public function test_permissions_needed_for_import_page()
20+
{
21+
$user = $this->users->viewer();
22+
$this->actingAs($user);
23+
24+
$resp = $this->get('/books');
25+
$this->withHtml($resp)->assertLinkNotExists(url('/import'));
26+
$resp = $this->get('/import');
27+
$resp->assertRedirect('/');
28+
29+
$this->permissions->grantUserRolePermissions($user, ['content-import']);
30+
31+
$resp = $this->get('/books');
32+
$this->withHtml($resp)->assertLinkExists(url('/import'));
33+
$resp = $this->get('/import');
34+
$resp->assertOk();
35+
$resp->assertSeeText('Select ZIP file to upload');
36+
}
37+
38+
public function test_zip_read_errors_are_shown_on_validation()
39+
{
40+
$invalidUpload = $this->files->uploadedImage('image.zip');
41+
42+
$this->asAdmin();
43+
$resp = $this->runImportFromFile($invalidUpload);
44+
$resp->assertRedirect('/import');
45+
46+
$resp = $this->followRedirects($resp);
47+
$resp->assertSeeText('Could not read ZIP file');
48+
}
49+
50+
public function test_error_shown_if_missing_data()
51+
{
52+
$zipFile = tempnam(sys_get_temp_dir(), 'bstest-');
53+
$zip = new ZipArchive();
54+
$zip->open($zipFile, ZipArchive::CREATE);
55+
$zip->addFromString('beans', 'cat');
56+
$zip->close();
57+
58+
$this->asAdmin();
59+
$upload = new UploadedFile($zipFile, 'upload.zip', 'application/zip', null, true);
60+
$resp = $this->runImportFromFile($upload);
61+
$resp->assertRedirect('/import');
62+
63+
$resp = $this->followRedirects($resp);
64+
$resp->assertSeeText('Could not find and decode ZIP data.json content.');
65+
}
66+
67+
public function test_error_shown_if_no_importable_key()
68+
{
69+
$this->asAdmin();
70+
$resp = $this->runImportFromFile($this->zipUploadFromData([
71+
'instance' => []
72+
]));
73+
74+
$resp->assertRedirect('/import');
75+
$resp = $this->followRedirects($resp);
76+
$resp->assertSeeText('ZIP file data has no expected book, chapter or page content.');
77+
}
78+
79+
public function test_zip_data_validation_messages_shown()
80+
{
81+
$this->asAdmin();
82+
$resp = $this->runImportFromFile($this->zipUploadFromData([
83+
'book' => [
84+
'id' => 4,
85+
'pages' => [
86+
'cat',
87+
[
88+
'name' => 'My inner page',
89+
'tags' => [
90+
[
91+
'value' => 5
92+
]
93+
],
94+
]
95+
],
96+
]
97+
]));
98+
99+
$resp->assertRedirect('/import');
100+
$resp = $this->followRedirects($resp);
101+
102+
$resp->assertSeeText('[book.name]: The name field is required.');
103+
$resp->assertSeeText('[book.pages.0.0]: Data object expected but "string" found.');
104+
$resp->assertSeeText('[book.pages.1.tags.0.name]: The name field is required.');
105+
$resp->assertSeeText('[book.pages.1.tags.0.value]: The value must be a string.');
106+
}
107+
108+
protected function runImportFromFile(UploadedFile $file): TestResponse
109+
{
110+
return $this->call('POST', '/import', [], [], ['file' => $file]);
111+
}
112+
113+
protected function zipUploadFromData(array $data): UploadedFile
114+
{
115+
$zipFile = tempnam(sys_get_temp_dir(), 'bstest-');
116+
117+
$zip = new ZipArchive();
118+
$zip->open($zipFile, ZipArchive::CREATE);
119+
$zip->addFromString('data.json', json_encode($data));
120+
$zip->close();
121+
122+
return new UploadedFile($zipFile, 'upload.zip', 'application/zip', null, true);
123+
}
124+
}

0 commit comments

Comments
 (0)