1
0
mirror of https://github.com/BookStackApp/BookStack.git synced 2025-07-28 17:02:04 +03:00

ZIP Imports: Added API test cases

This commit is contained in:
Dan Brown
2025-07-18 14:05:32 +01:00
parent d55684531f
commit 73025719a4
7 changed files with 214 additions and 11 deletions

View File

@ -26,9 +26,11 @@ class ImportApiController extends ApiController
*/
public function list(): JsonResponse
{
$imports = $this->imports->getVisibleImports()->all();
$query = $this->imports->queryVisible();
return response()->json($imports);
return $this->apiListingResponse($query, [
'id', 'name', 'size', 'type', 'created_by', 'created_at', 'updated_at'
]);
}
/**
@ -44,7 +46,7 @@ class ImportApiController extends ApiController
try {
$import = $this->imports->storeFromUpload($file);
} catch (ZipValidationException $exception) {
$message = "ZIP upload failed with the following validation errors: \n" . implode("\n", $exception->errors);
$message = "ZIP upload failed with the following validation errors: \n" . $this->formatErrors($exception->errors);
return $this->jsonError($message, 422);
}
@ -53,11 +55,15 @@ class ImportApiController extends ApiController
/**
* Read details of a pending ZIP import.
* The "details" property contains high-level metadata regarding the ZIP import content,
* and the structure of this will change depending on import "type".
*/
public function read(int $id): JsonResponse
{
$import = $this->imports->findVisible($id);
$import->setAttribute('details', $import->decodeMetadata());
return response()->json($import);
}
@ -82,7 +88,7 @@ class ImportApiController extends ApiController
try {
$entity = $this->imports->runImport($import, $parent);
} catch (ZipImportException $exception) {
$message = "ZIP import failed with the following errors: \n" . implode("\n", $exception->errors);
$message = "ZIP import failed with the following errors: \n" . $this->formatErrors($exception->errors);
return $this->jsonError($message);
}
@ -112,4 +118,17 @@ class ImportApiController extends ApiController
],
];
}
protected function formatErrors(array $errors): string
{
$parts = [];
foreach ($errors as $key => $error) {
if (is_string($key)) {
$parts[] = "[{$key}] {$error}";
} else {
$parts[] = $error;
}
}
return implode("\n", $parts);
}
}

View File

@ -28,6 +28,8 @@ class Import extends Model implements Loggable
{
use HasFactory;
protected $hidden = ['metadata'];
public function getSizeString(): string
{
$mb = round($this->size / 1000000, 2);

View File

@ -17,6 +17,7 @@ use BookStack\Exports\ZipExports\ZipExportValidator;
use BookStack\Exports\ZipExports\ZipImportRunner;
use BookStack\Facades\Activity;
use BookStack\Uploads\FileStorage;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Collection;
use Illuminate\Support\Facades\DB;
use Symfony\Component\HttpFoundation\File\UploadedFile;
@ -34,6 +35,11 @@ class ImportRepo
* @return Collection<Import>
*/
public function getVisibleImports(): Collection
{
return $this->queryVisible()->get();
}
public function queryVisible(): Builder
{
$query = Import::query();
@ -41,7 +47,7 @@ class ImportRepo
$query->where('created_by', user()->id);
}
return $query->get();
return $query;
}
public function findVisible(int $id): Import

View File

@ -8,7 +8,7 @@ use Illuminate\Http\JsonResponse;
abstract class ApiController extends Controller
{
protected $rules = [];
protected array $rules = [];
/**
* Provide a paginated listing JSON response in a standard format

View File

@ -24,6 +24,7 @@ class ImportFactory extends Factory
'path' => 'uploads/files/imports/' . Str::random(10) . '.zip',
'name' => $this->faker->words(3, true),
'type' => 'book',
'size' => rand(1, 1001),
'metadata' => '{"name": "My book"}',
'created_at' => User::factory(),
];

View File

@ -88,11 +88,11 @@ Route::get('roles/{id}', [RoleApiController::class, 'read']);
Route::put('roles/{id}', [RoleApiController::class, 'update']);
Route::delete('roles/{id}', [RoleApiController::class, 'delete']);
Route::get('import', [ExportControllers\ImportApiController::class, 'list']);
Route::post('import', [ExportControllers\ImportApiController::class, 'upload']);
Route::get('import/{id}', [ExportControllers\ImportApiController::class, 'read']);
Route::post('import/{id}', [ExportControllers\ImportApiController::class, 'run']);
Route::delete('import/{id}', [ExportControllers\ImportApiController::class, 'delete']);
Route::get('imports', [ExportControllers\ImportApiController::class, 'list']);
Route::post('imports', [ExportControllers\ImportApiController::class, 'upload']);
Route::get('imports/{id}', [ExportControllers\ImportApiController::class, 'read']);
Route::post('imports/{id}', [ExportControllers\ImportApiController::class, 'run']);
Route::delete('imports/{id}', [ExportControllers\ImportApiController::class, 'delete']);
Route::get('recycle-bin', [EntityControllers\RecycleBinApiController::class, 'list']);
Route::put('recycle-bin/{deletionId}', [EntityControllers\RecycleBinApiController::class, 'restore']);

View File

@ -0,0 +1,175 @@
<?php
namespace Api;
use BookStack\Entities\Models\Page;
use BookStack\Exports\Import;
use Tests\Api\TestsApi;
use Tests\Exports\ZipTestHelper;
use Tests\TestCase;
class ImportsApiTest extends TestCase
{
use TestsApi;
protected string $baseEndpoint = '/api/imports';
public function test_upload_and_run(): void
{
$book = $this->entities->book();
$zip = ZipTestHelper::zipUploadFromData([
'page' => [
'name' => 'My API import page',
'tags' => [
[
'name' => 'My api tag',
'value' => 'api test value'
]
],
],
]);
$resp = $this->actingAsApiAdmin()->call('POST', $this->baseEndpoint, [], [], ['file' => $zip]);
$resp->assertStatus(200);
$importId = $resp->json('id');
$import = Import::query()->findOrFail($importId);
$this->assertEquals('page', $import->type);
$resp = $this->post($this->baseEndpoint . "/{$import->id}", [
'parent_type' => 'book',
'parent_id' => $book->id,
]);
$resp->assertJson([
'name' => 'My API import page',
'book_id' => $book->id,
]);
$page = Page::query()->where('name', '=', 'My API import page')->first();
$this->assertEquals('My api tag', $page->tags()->first()->name);
}
public function test_upload_validation_error(): void
{
$zip = ZipTestHelper::zipUploadFromData([
'page' => [
'tags' => [
[
'name' => 'My api tag',
'value' => 'api test value'
]
],
],
]);
$resp = $this->actingAsApiAdmin()->call('POST', $this->baseEndpoint, [], [], ['file' => $zip]);
$resp->assertStatus(422);
$message = $resp->json('message');
$this->assertStringContainsString('ZIP upload failed with the following validation errors:', $message);
$this->assertStringContainsString('[page.name] The name field is required.', $message);
}
public function test_list(): void
{
$imports = Import::factory()->count(10)->create();
$resp = $this->actingAsApiAdmin()->get($this->baseEndpoint);
$resp->assertJsonCount(10, 'data');
$resp->assertJsonPath('total', 10);
$firstImport = $imports->first();
$resp = $this->actingAsApiAdmin()->get($this->baseEndpoint . '?filter[id]=' . $firstImport->id);
$resp->assertJsonCount(1, 'data');
$resp->assertJsonPath('data.0.id', $firstImport->id);
$resp->assertJsonPath('data.0.name', $firstImport->name);
$resp->assertJsonPath('data.0.size', $firstImport->size);
$resp->assertJsonPath('data.0.type', $firstImport->type);
}
public function test_list_visibility_limited(): void
{
$user = $this->users->editor();
$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->actingAsForApi($user)->get($this->baseEndpoint);
$resp->assertJsonCount(1, 'data');
$resp->assertJsonPath('data.0.name', 'MySuperUserImport');
$this->permissions->grantUserRolePermissions($user, ['settings-manage']);
$resp = $this->actingAsForApi($user)->get($this->baseEndpoint);
$resp->assertJsonCount(2, 'data');
$resp->assertJsonPath('data.1.name', 'MySuperAdminImport');
}
public function test_read(): void
{
$zip = ZipTestHelper::zipUploadFromData([
'book' => [
'name' => 'My API import book',
'pages' => [
[
'name' => 'My import page',
'tags' => [
[
'name' => 'My api tag',
'value' => 'api test value'
]
]
]
],
],
]);
$resp = $this->actingAsApiAdmin()->call('POST', $this->baseEndpoint, [], [], ['file' => $zip]);
$resp->assertStatus(200);
$resp = $this->get($this->baseEndpoint . "/{$resp->json('id')}");
$resp->assertStatus(200);
$resp->assertJsonPath('details.name', 'My API import book');
$resp->assertJsonPath('details.pages.0.name', 'My import page');
$resp->assertJsonPath('details.pages.0.tags.0.name', 'My api tag');
$resp->assertJsonMissingPath('metadata');
}
public function test_delete(): void
{
$import = Import::factory()->create();
$resp = $this->actingAsApiAdmin()->delete($this->baseEndpoint . "/{$import->id}");
$resp->assertStatus(204);
}
public function test_content_import_permissions_needed(): void
{
$user = $this->users->viewer();
$this->permissions->grantUserRolePermissions($user, ['access-api']);
$this->actingAsForApi($user);
$requests = [
['GET', $this->baseEndpoint],
['POST', $this->baseEndpoint],
['GET', $this->baseEndpoint . "/1"],
['POST', $this->baseEndpoint . "/1"],
['DELETE', $this->baseEndpoint . "/1"],
];
foreach ($requests as $request) {
[$method, $endpoint] = $request;
$resp = $this->json($method, $endpoint);
$resp->assertStatus(403);
}
$this->permissions->grantUserRolePermissions($user, ['content-import']);
foreach ($requests as $request) {
[$method, $endpoint] = $request;
$resp = $this->call($method, $endpoint);
$this->assertNotEquals(403, $resp->status(), "A {$method} request to {$endpoint} returned 403");
}
}
}