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:
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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(),
|
||||
];
|
||||
|
@ -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']);
|
||||
|
175
tests/Api/ImportsApiTest.php
Normal file
175
tests/Api/ImportsApiTest.php
Normal 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");
|
||||
}
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user