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:
Dan Brown 2024-11-02 14:51:04 +00:00
parent c4ec50d437
commit 259aa829d4
No known key found for this signature in database
GPG Key ID: 46D9F943C24A2EF9
7 changed files with 164 additions and 16 deletions

View File

@ -17,7 +17,9 @@ class ImportController extends Controller
{
// 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)
@ -31,13 +33,21 @@ class ImportController extends Controller
$errors = (new ZipExportValidator($zipPath))->validate();
if ($errors) {
dd($errors);
session()->flash('validation_errors', $errors);
return redirect('/import');
}
dd('passed');
// TODO - Read existing ZIP upload and send through validator
// TODO - If invalid, return user with errors
// 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
}
}

View File

@ -18,21 +18,21 @@ class ZipExportValidator
{
// Validate file exists
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
$zip = new \ZipArchive();
$opened = $zip->open($this->zipPath, ZipArchive::RDONLY);
if ($opened !== true) {
return ['format' => "Could not read ZIP file"];
return ['format' => trans('errors.import_zip_cant_read')];
}
// Validate json data exists, including metadata
$jsonData = $zip->getFromName('data.json') ?: '';
$importData = json_decode($jsonData, true);
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);
@ -47,9 +47,10 @@ class ZipExportValidator
$modelErrors = ZipExportPage::validate($helper, $importData['page']);
$keyPrefix = 'page';
} 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);
}

View File

@ -45,6 +45,9 @@ return [
'default_template_select' => 'Select a template page',
'import' => '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' => 'Permissions',

View File

@ -105,6 +105,11 @@ return [
'app_down' => ':appName is down right now',
'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_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',

View File

@ -106,7 +106,7 @@ return [
'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_model_expected' => 'Data object expected but ":type" found',
'zip_model_expected' => 'Data object expected but ":type" found.',
// Custom validation lines
'custom' => [

View File

@ -9,14 +9,10 @@
<form action="{{ url('/import') }}" enctype="multipart/form-data" method="POST">
{{ csrf_field() }}
<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">
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>
<p class="flex min-width-l text-muted mb-s">{{ trans('entities.import_desc') }}</p>
<div class="flex-none min-width-l flex-container-row justify-flex-end">
<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"
accept=".zip,application/zip,application/x-zip-compressed"
name="file"
@ -27,6 +23,15 @@
</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">
<a href="{{ url('/books') }}" class="button outline">{{ trans('common.cancel') }}</a>
<button type="submit" class="button">{{ trans('entities.import_validate') }}</button>

View File

@ -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);
}
}