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.
This commit is contained in:
parent
c4ec50d437
commit
259aa829d4
|
@ -17,7 +17,9 @@ class ImportController extends Controller
|
||||||
{
|
{
|
||||||
// TODO - Show existing imports for user (or for all users if admin-level user)
|
// TODO - Show existing imports for user (or for all users if admin-level user)
|
||||||
|
|
||||||
return view('exports.import');
|
return view('exports.import', [
|
||||||
|
'zipErrors' => session()->pull('validation_errors') ?? [],
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function upload(Request $request)
|
public function upload(Request $request)
|
||||||
|
@ -31,13 +33,21 @@ class ImportController extends Controller
|
||||||
|
|
||||||
$errors = (new ZipExportValidator($zipPath))->validate();
|
$errors = (new ZipExportValidator($zipPath))->validate();
|
||||||
if ($errors) {
|
if ($errors) {
|
||||||
dd($errors);
|
session()->flash('validation_errors', $errors);
|
||||||
|
return redirect('/import');
|
||||||
}
|
}
|
||||||
|
|
||||||
dd('passed');
|
dd('passed');
|
||||||
// TODO - Read existing ZIP upload and send through validator
|
|
||||||
// TODO - If invalid, return user with errors
|
|
||||||
// TODO - Upload to storage
|
// TODO - Upload to storage
|
||||||
// TODO - Store info/results from validator
|
// TODO - Store info/results for display:
|
||||||
|
// - zip_path
|
||||||
|
// - name (From name of thing being imported)
|
||||||
|
// - size
|
||||||
|
// - book_count
|
||||||
|
// - chapter_count
|
||||||
|
// - page_count
|
||||||
|
// - created_by
|
||||||
|
// - created_at/updated_at
|
||||||
// TODO - Send user to next import stage
|
// TODO - Send user to next import stage
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,21 +18,21 @@ class ZipExportValidator
|
||||||
{
|
{
|
||||||
// Validate file exists
|
// Validate file exists
|
||||||
if (!file_exists($this->zipPath) || !is_readable($this->zipPath)) {
|
if (!file_exists($this->zipPath) || !is_readable($this->zipPath)) {
|
||||||
return ['format' => "Could not read ZIP file"];
|
return ['format' => trans('errors.import_zip_cant_read')];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate file is valid zip
|
// Validate file is valid zip
|
||||||
$zip = new \ZipArchive();
|
$zip = new \ZipArchive();
|
||||||
$opened = $zip->open($this->zipPath, ZipArchive::RDONLY);
|
$opened = $zip->open($this->zipPath, ZipArchive::RDONLY);
|
||||||
if ($opened !== true) {
|
if ($opened !== true) {
|
||||||
return ['format' => "Could not read ZIP file"];
|
return ['format' => trans('errors.import_zip_cant_read')];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate json data exists, including metadata
|
// Validate json data exists, including metadata
|
||||||
$jsonData = $zip->getFromName('data.json') ?: '';
|
$jsonData = $zip->getFromName('data.json') ?: '';
|
||||||
$importData = json_decode($jsonData, true);
|
$importData = json_decode($jsonData, true);
|
||||||
if (!$importData) {
|
if (!$importData) {
|
||||||
return ['format' => "Could not find and decode ZIP data.json content"];
|
return ['format' => trans('errors.import_zip_cant_decode_data')];
|
||||||
}
|
}
|
||||||
|
|
||||||
$helper = new ZipValidationHelper($zip);
|
$helper = new ZipValidationHelper($zip);
|
||||||
|
@ -47,9 +47,10 @@ class ZipExportValidator
|
||||||
$modelErrors = ZipExportPage::validate($helper, $importData['page']);
|
$modelErrors = ZipExportPage::validate($helper, $importData['page']);
|
||||||
$keyPrefix = 'page';
|
$keyPrefix = 'page';
|
||||||
} else {
|
} else {
|
||||||
return ['format' => "ZIP file has no book, chapter or page data"];
|
return ['format' => trans('errors.import_zip_no_data')];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
return $this->flattenModelErrors($modelErrors, $keyPrefix);
|
return $this->flattenModelErrors($modelErrors, $keyPrefix);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -45,6 +45,9 @@ return [
|
||||||
'default_template_select' => 'Select a template page',
|
'default_template_select' => 'Select a template page',
|
||||||
'import' => 'Import',
|
'import' => 'Import',
|
||||||
'import_validate' => 'Validate Import',
|
'import_validate' => 'Validate Import',
|
||||||
|
'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.',
|
||||||
|
'import_zip_select' => 'Select ZIP file to upload',
|
||||||
|
'import_zip_validation_errors' => 'Errors were detected while validating the provided ZIP file:',
|
||||||
|
|
||||||
// Permissions and restrictions
|
// Permissions and restrictions
|
||||||
'permissions' => 'Permissions',
|
'permissions' => 'Permissions',
|
||||||
|
|
|
@ -105,6 +105,11 @@ return [
|
||||||
'app_down' => ':appName is down right now',
|
'app_down' => ':appName is down right now',
|
||||||
'back_soon' => 'It will be back up soon.',
|
'back_soon' => 'It will be back up soon.',
|
||||||
|
|
||||||
|
// Import
|
||||||
|
'import_zip_cant_read' => 'Could not read ZIP file.',
|
||||||
|
'import_zip_cant_decode_data' => 'Could not find and decode ZIP data.json content.',
|
||||||
|
'import_zip_no_data' => 'ZIP file data has no expected book, chapter or page content.',
|
||||||
|
|
||||||
// API errors
|
// API errors
|
||||||
'api_no_authorization_found' => 'No authorization token found on the request',
|
'api_no_authorization_found' => 'No authorization token found on the request',
|
||||||
'api_bad_authorization_format' => 'An authorization token was found on the request but the format appeared incorrect',
|
'api_bad_authorization_format' => 'An authorization token was found on the request but the format appeared incorrect',
|
||||||
|
|
|
@ -106,7 +106,7 @@ return [
|
||||||
'uploaded' => 'The file could not be uploaded. The server may not accept files of this size.',
|
'uploaded' => 'The file could not be uploaded. The server may not accept files of this size.',
|
||||||
|
|
||||||
'zip_file' => 'The :attribute needs to reference a file within the ZIP.',
|
'zip_file' => 'The :attribute needs to reference a file within the ZIP.',
|
||||||
'zip_model_expected' => 'Data object expected but ":type" found',
|
'zip_model_expected' => 'Data object expected but ":type" found.',
|
||||||
|
|
||||||
// Custom validation lines
|
// Custom validation lines
|
||||||
'custom' => [
|
'custom' => [
|
||||||
|
|
|
@ -9,14 +9,10 @@
|
||||||
<form action="{{ url('/import') }}" enctype="multipart/form-data" method="POST">
|
<form action="{{ url('/import') }}" enctype="multipart/form-data" method="POST">
|
||||||
{{ csrf_field() }}
|
{{ csrf_field() }}
|
||||||
<div class="flex-container-row justify-space-between wrap gap-x-xl gap-y-s">
|
<div class="flex-container-row justify-space-between wrap gap-x-xl gap-y-s">
|
||||||
<p class="flex min-width-l text-muted mb-s">
|
<p class="flex min-width-l text-muted mb-s">{{ trans('entities.import_desc') }}</p>
|
||||||
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.
|
|
||||||
</p>
|
|
||||||
<div class="flex-none min-width-l flex-container-row justify-flex-end">
|
<div class="flex-none min-width-l flex-container-row justify-flex-end">
|
||||||
<div class="mb-m">
|
<div class="mb-m">
|
||||||
<label for="file">Select ZIP file to upload</label>
|
<label for="file">{{ trans('entities.import_zip_select') }}</label>
|
||||||
<input type="file"
|
<input type="file"
|
||||||
accept=".zip,application/zip,application/x-zip-compressed"
|
accept=".zip,application/zip,application/x-zip-compressed"
|
||||||
name="file"
|
name="file"
|
||||||
|
@ -27,6 +23,15 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@if(count($zipErrors) > 0)
|
||||||
|
<p class="mb-xs"><strong class="text-neg">{{ trans('entities.import_zip_validation_errors') }}</strong></p>
|
||||||
|
<ul class="mb-m">
|
||||||
|
@foreach($zipErrors as $key => $error)
|
||||||
|
<li><strong class="text-neg">[{{ $key }}]</strong>: {{ $error }}</li>
|
||||||
|
@endforeach
|
||||||
|
</ul>
|
||||||
|
@endif
|
||||||
|
|
||||||
<div class="text-right">
|
<div class="text-right">
|
||||||
<a href="{{ url('/books') }}" class="button outline">{{ trans('common.cancel') }}</a>
|
<a href="{{ url('/books') }}" class="button outline">{{ trans('common.cancel') }}</a>
|
||||||
<button type="submit" class="button">{{ trans('entities.import_validate') }}</button>
|
<button type="submit" class="button">{{ trans('entities.import_validate') }}</button>
|
||||||
|
|
|
@ -0,0 +1,124 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace Tests\Exports;
|
||||||
|
|
||||||
|
use Illuminate\Http\UploadedFile;
|
||||||
|
use Illuminate\Testing\TestResponse;
|
||||||
|
use Tests\TestCase;
|
||||||
|
use ZipArchive;
|
||||||
|
|
||||||
|
class ZipImportTest extends TestCase
|
||||||
|
{
|
||||||
|
public function test_import_page_view()
|
||||||
|
{
|
||||||
|
$resp = $this->asAdmin()->get('/import');
|
||||||
|
$resp->assertSee('Import');
|
||||||
|
$this->withHtml($resp)->assertElementExists('form input[type="file"][name="file"]');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_permissions_needed_for_import_page()
|
||||||
|
{
|
||||||
|
$user = $this->users->viewer();
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$resp = $this->get('/books');
|
||||||
|
$this->withHtml($resp)->assertLinkNotExists(url('/import'));
|
||||||
|
$resp = $this->get('/import');
|
||||||
|
$resp->assertRedirect('/');
|
||||||
|
|
||||||
|
$this->permissions->grantUserRolePermissions($user, ['content-import']);
|
||||||
|
|
||||||
|
$resp = $this->get('/books');
|
||||||
|
$this->withHtml($resp)->assertLinkExists(url('/import'));
|
||||||
|
$resp = $this->get('/import');
|
||||||
|
$resp->assertOk();
|
||||||
|
$resp->assertSeeText('Select ZIP file to upload');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_zip_read_errors_are_shown_on_validation()
|
||||||
|
{
|
||||||
|
$invalidUpload = $this->files->uploadedImage('image.zip');
|
||||||
|
|
||||||
|
$this->asAdmin();
|
||||||
|
$resp = $this->runImportFromFile($invalidUpload);
|
||||||
|
$resp->assertRedirect('/import');
|
||||||
|
|
||||||
|
$resp = $this->followRedirects($resp);
|
||||||
|
$resp->assertSeeText('Could not read ZIP file');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_error_shown_if_missing_data()
|
||||||
|
{
|
||||||
|
$zipFile = tempnam(sys_get_temp_dir(), 'bstest-');
|
||||||
|
$zip = new ZipArchive();
|
||||||
|
$zip->open($zipFile, ZipArchive::CREATE);
|
||||||
|
$zip->addFromString('beans', 'cat');
|
||||||
|
$zip->close();
|
||||||
|
|
||||||
|
$this->asAdmin();
|
||||||
|
$upload = new UploadedFile($zipFile, 'upload.zip', 'application/zip', null, true);
|
||||||
|
$resp = $this->runImportFromFile($upload);
|
||||||
|
$resp->assertRedirect('/import');
|
||||||
|
|
||||||
|
$resp = $this->followRedirects($resp);
|
||||||
|
$resp->assertSeeText('Could not find and decode ZIP data.json content.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_error_shown_if_no_importable_key()
|
||||||
|
{
|
||||||
|
$this->asAdmin();
|
||||||
|
$resp = $this->runImportFromFile($this->zipUploadFromData([
|
||||||
|
'instance' => []
|
||||||
|
]));
|
||||||
|
|
||||||
|
$resp->assertRedirect('/import');
|
||||||
|
$resp = $this->followRedirects($resp);
|
||||||
|
$resp->assertSeeText('ZIP file data has no expected book, chapter or page content.');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function test_zip_data_validation_messages_shown()
|
||||||
|
{
|
||||||
|
$this->asAdmin();
|
||||||
|
$resp = $this->runImportFromFile($this->zipUploadFromData([
|
||||||
|
'book' => [
|
||||||
|
'id' => 4,
|
||||||
|
'pages' => [
|
||||||
|
'cat',
|
||||||
|
[
|
||||||
|
'name' => 'My inner page',
|
||||||
|
'tags' => [
|
||||||
|
[
|
||||||
|
'value' => 5
|
||||||
|
]
|
||||||
|
],
|
||||||
|
]
|
||||||
|
],
|
||||||
|
]
|
||||||
|
]));
|
||||||
|
|
||||||
|
$resp->assertRedirect('/import');
|
||||||
|
$resp = $this->followRedirects($resp);
|
||||||
|
|
||||||
|
$resp->assertSeeText('[book.name]: The name field is required.');
|
||||||
|
$resp->assertSeeText('[book.pages.0.0]: Data object expected but "string" found.');
|
||||||
|
$resp->assertSeeText('[book.pages.1.tags.0.name]: The name field is required.');
|
||||||
|
$resp->assertSeeText('[book.pages.1.tags.0.value]: The value must be a string.');
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue