Compare commits

..

6 Commits

Author SHA1 Message Date
philipp lang 8bf17784cc Add deferred upload 2024-01-03 15:47:41 +01:00
philipp lang c00ee87082 Add authroization with actual user 2024-01-03 13:42:29 +01:00
philipp lang 1b2b148af5 Add auth 2024-01-03 13:07:03 +01:00
philipp lang 14ca7d312f Add config path 2024-01-03 12:19:15 +01:00
philipp lang 17afb48028 Add Workbench config 2024-01-03 11:10:59 +01:00
philipp lang 22d7841d5a Add Model rule 2024-01-03 02:12:26 +01:00
27 changed files with 608 additions and 237 deletions

View File

@ -5,10 +5,16 @@
"license": "MIT",
"autoload": {
"psr-4": {
"Zoomyboy\\MedialibraryHelper\\Tests\\": "tests/",
"Zoomyboy\\MedialibraryHelper\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Zoomyboy\\MedialibraryHelper\\Tests\\": "tests/",
"Workbench\\App\\": "tests/workbench/app/",
"Database\\Factories\\": "tests/workbench/database/factories/"
}
},
"authors": [
{
"name": "Philipp Lang",
@ -28,7 +34,20 @@
},
"scripts": {
"post-autoload-dump": [
"@clear",
"@prepare",
"@php vendor/bin/testbench package:discover --ansi"
],
"clear": "@php vendor/bin/testbench package:purge-skeleton --ansi",
"prepare": "@php vendor/bin/testbench package:discover --ansi",
"build": "@php vendor/bin/testbench workbench:build --ansi",
"serve": [
"Composer\\Config::disableProcessTimeout",
"@build",
"@php vendor/bin/testbench serve"
],
"test": [
"@php vendor/bin/pest"
]
},
"config": {

17
composer.lock generated
View File

@ -8682,16 +8682,16 @@
"packages-dev": [
{
"name": "fakerphp/faker",
"version": "v1.23.0",
"version": "v1.23.1",
"source": {
"type": "git",
"url": "https://github.com/FakerPHP/Faker.git",
"reference": "e3daa170d00fde61ea7719ef47bb09bb8f1d9b01"
"reference": "bfb4fe148adbf78eff521199619b93a52ae3554b"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/FakerPHP/Faker/zipball/e3daa170d00fde61ea7719ef47bb09bb8f1d9b01",
"reference": "e3daa170d00fde61ea7719ef47bb09bb8f1d9b01",
"url": "https://api.github.com/repos/FakerPHP/Faker/zipball/bfb4fe148adbf78eff521199619b93a52ae3554b",
"reference": "bfb4fe148adbf78eff521199619b93a52ae3554b",
"shasum": ""
},
"require": {
@ -8717,11 +8717,6 @@
"ext-mbstring": "Required for multibyte Unicode string functionality."
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "v1.21-dev"
}
},
"autoload": {
"psr-4": {
"Faker\\": "src/Faker/"
@ -8744,9 +8739,9 @@
],
"support": {
"issues": "https://github.com/FakerPHP/Faker/issues",
"source": "https://github.com/FakerPHP/Faker/tree/v1.23.0"
"source": "https://github.com/FakerPHP/Faker/tree/v1.23.1"
},
"time": "2023-06-12T08:44:38+00:00"
"time": "2024-01-02T13:46:09+00:00"
},
{
"name": "hamcrest/hamcrest-php",

6
config/media-library.php Normal file
View File

@ -0,0 +1,6 @@
<?php
return [
'temp_disk' => 'temp',
'middleware' => ['web', 'auth:web'],
];

50
src/DeferredMediaData.php Normal file
View File

@ -0,0 +1,50 @@
<?php
namespace Zoomyboy\MedialibraryHelper;
use Illuminate\Support\Facades\Storage;
use Illuminate\Support\Str;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Data;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\MediaCollections\MediaCollection;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
#[MapInputName(SnakeCaseMapper::class)]
#[MapOutputName(SnakeCaseMapper::class)]
class DeferredMediaData extends Data
{
public ?int $id;
public string $originalUrl;
public int $size;
public string $name;
public string $collectionName;
public string $fileName;
public string $mimeType;
public bool $fallback = false;
public bool $isDeferred = true;
public static function fromPath(string $path, MediaCollection $collection): self
{
$file = new MediaFile(Storage::disk(config('media-library.temp_disk'))->path($path));
return static::withoutMagicalCreationFrom([
'collection_name' => $collection->name,
'original_url' => Storage::disk(config('media-library.temp_disk'))->url($path),
'size' => $file->getSize(),
'name' => $file->getBasename(),
'file_name' => $file->getFilename(),
'mime_type' => Storage::disk(config('media-library.temp_disk'))->mimeType($path),
'path' => $path,
]);
}
}

View File

@ -5,16 +5,14 @@ namespace Zoomyboy\MedialibraryHelper;
use Illuminate\Foundation\Auth\Access\AuthorizesRequests;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Validation\Rule;
use Illuminate\Validation\ValidationException;
use Spatie\Image\Image;
use Spatie\LaravelData\DataCollection;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\MediaCollections\Exceptions\InvalidBase64Data;
use Spatie\MediaLibrary\MediaCollections\FileAdder;
use Spatie\MediaLibrary\MediaCollections\MediaCollection;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Symfony\Component\HttpFoundation\File\File;
use Zoomyboy\MedialibraryHelper\Rules\ModelRule;
use Illuminate\Support\Facades\Storage;
class MediaController
{
@ -23,12 +21,15 @@ class MediaController
public function store(Request $request)
{
$request->validate([
'model' => ['required', 'string', Rule::in(app('media-library-helpers')->keys())],
'id' => 'required',
'parent' => ['required', new ModelRule()],
]);
$model = $this->validateModel($request);
$collection = $model->getMediaCollection($request->input('collection'));
if (is_null($request->input('parent.id'))) {
return $this->storeDeferred($request);
}
$model = ModelRule::getModel($request->input('parent'));
$collection = ModelRule::getCollection($request->input('parent'));
$isSingle = 1 === $collection->collectionSizeLimit;
$this->authorize('storeMedia', [$model, $collection->name]);
@ -65,6 +66,37 @@ class MediaController
return $isSingle ? MediaData::from($medias->first()) : MediaData::collection($medias);
}
protected function storeDeferred(Request $request)
{
$modelName = ModelRule::getModelClassName($request->input('parent'));
$collection = ModelRule::getCollection($request->input('parent'));
$isSingle = 1 === $collection->collectionSizeLimit;
$request->validate($isSingle ? [
'payload' => 'array',
'payload.name' => 'required|string|regex:/\..*$/|max:255',
'payload.content' => 'required|string',
] : [
'payload' => 'required|array|min:1',
'payload.*' => 'array',
'payload.*.name' => 'required|string|regex:/\..*$/|max:255',
'payload.*.content' => 'required|string',
]);
$content = $isSingle ? [$request->input('payload')] : $request->input('payload');
$tempPaths = collect($content)->map(function ($c) use ($collection, $modelName) {
$file = new MediaFile($c['name']);
$tmpFile = $this->storeTemporaryFile($c['content'], $collection);
Storage::disk(config('media-library.temp_disk'))->move($tmpFile, 'media-library/' . $file->getFilename());
return 'media-library/' . $file->getFilename();
});
$this->authorize('storeMedia', [$modelName, null, $collection->name]);
return $isSingle ? DeferredMediaData::fromPath($tempPaths->first(), $collection) : DeferredMediaData::collectionFromPaths($tempPaths, $collection);
}
public function update(Request $request, Media $media): MediaData
{
$this->authorize('updateMedia', [$media->model, $media->collection_name]);
@ -116,27 +148,7 @@ class MediaController
return property_exists($collection, $callback);
}
protected function validateModel(Request $request): HasMedia
{
$model = app('media-library-helpers')->get($request->input('model'));
$request->validate([
'collection' => [
'required',
'string',
Rule::in((new $model())->getRegisteredMediaCollections()->pluck('name')),
],
]);
$model = $model::find($request->input('id'));
if (!$model) {
throw ValidationException::withMessages(['model' => 'nicht gefunden']);
}
return $model;
}
protected function fileAdderFromData($model, $data, $collection): FileAdder
private function storeTemporaryFile(string $data, MediaCollection $collection): string
{
$maxWidth = $collection->runCallback('maxWidth', 9);
if (str_contains($data, ';base64')) {
@ -156,13 +168,20 @@ class MediaController
throw InvalidBase64Data::create();
}
$tmpFile = tempnam(sys_get_temp_dir(), 'media-library');
file_put_contents($tmpFile, $binaryData);
$tmpFile = 'media-library/' . str()->uuid()->toString();
Storage::disk(config('media-library.temp_disk'))->put($tmpFile, $binaryData);
if (null !== $maxWidth && 'image/jpeg' === mime_content_type($tmpFile)) {
Image::load($tmpFile)->width($maxWidth)->save();
if (null !== $maxWidth && 'image/jpeg' === Storage::disk(config('media-library.temp_disk'))->mimeType($tmpFile)) {
Image::load(Storage::disk(config('media-library.temp_disk'))->path($tmpFile))->width($maxWidth)->save();
}
return $model->addMedia($tmpFile);
return $tmpFile;
}
protected function fileAdderFromData($model, $data, $collection): FileAdder
{
$tmpFile = $this->storeTemporaryFile($data, $collection);
return $model->addMediaFromDisk($tmpFile, config('media-library.temp_disk'));
}
}

View File

@ -37,11 +37,14 @@ class MediaData extends Data
public bool $fallback = false;
public bool $isDeferred = false;
public static function fromMedia(Media $media): self
{
$conversions = collect($media->getMediaConversionNames())->flip()->map(fn ($integer, $conversion) => $media->hasGeneratedConversion($conversion)
? ['original_url' => $media->getFullUrl($conversion)]
: null,
$conversions = collect($media->getMediaConversionNames())->flip()->map(
fn ($integer, $conversion) => $media->hasGeneratedConversion($conversion)
? ['original_url' => $media->getFullUrl($conversion)]
: null,
);
return self::withoutMagicalCreationFrom([

89
src/Rules/ModelRule.php Normal file
View File

@ -0,0 +1,89 @@
<?php
namespace Zoomyboy\MedialibraryHelper\Rules;
use Illuminate\Contracts\Validation\InvokableRule;
use Spatie\MediaLibrary\HasMedia;
use Illuminate\Validation\Factory;
use Illuminate\Validation\Rule;
use Spatie\MediaLibrary\MediaCollections\MediaCollection;
class ModelRule implements InvokableRule
{
public ?string $collection;
public ?int $id;
public ?string $model;
/**
* @param array{?id: int, ?collection: string, ?model: string} $attribute
*/
public function __invoke($attribute, $value, $fail)
{
app(Factory::class)->make([$attribute => $value], [
"{$attribute}.id" => 'nullable|integer|gt:0',
"{$attribute}.model" => ['required', 'string', Rule::in(app('media-library-helpers')->keys())],
"{$attribute}.collection" => 'required|string',
])->validate();
$this->model = data_get($value, 'model');
$this->id = data_get($value, 'id');
$this->collection = data_get($value, 'collection');
if (is_null($this->id)) {
$this->validateDeferred($attribute, $value);
return;
}
$model = app('media-library-helpers')->get($this->model);
app(Factory::class)->make([$attribute => $value], [
"{$attribute}.collection" => ['required', Rule::in((new $model())->getRegisteredMediaCollections()->pluck('name'))],
"{$attribute}.id" => ['required', 'exists:' . (new $model)->getTable() . ',id'],
])->validate();
}
public function validateDeferred($attribute, $value): void
{
app(Factory::class)->make([$attribute => $value], [
"{$attribute}.model" => ['required', 'string', Rule::in(app('media-library-helpers')->keys())],
"{$attribute}.collection" => 'required|string',
])->validate();
$model = app('media-library-helpers')->get($this->model);
app(Factory::class)->make([$attribute => $value], [
"{$attribute}.collection" => ['required', Rule::in((new $model())->getRegisteredMediaCollections()->pluck('name'))],
])->validate();
}
/**
* @param array{?id: int, ?collection: string, ?model: string} $modelParam
*/
public static function getModel($modelParam): HasMedia
{
$model = static::getModelClassName($modelParam);
return $model::find($modelParam['id']);
}
/**
* @param array{?id: int, ?collection: string, ?model: string} $modelParam
* @return class-string<HasMedia>
*/
public static function getModelClassName($modelParam): string
{
return app('media-library-helpers')->get($modelParam['model']);
}
/**
* @param array{?id: int, ?collection: string, ?model: string} $modelParam
*/
public static function getCollection($modelParam): MediaCollection
{
$className = static::getModelClassName($modelParam);
return (new $className)->getMediaCollection($modelParam['collection']);
}
}

View File

@ -12,6 +12,8 @@ class ServiceProvider extends BaseServiceProvider
{
app()->bind('media-library-helpers', fn () => collect([]));
app()->singleton(CollectionExtension::class, fn () => new CollectionExtension());
$this->mergeConfigFrom(__DIR__ . '/../config/media-library.php', 'media-library');
}
public function boot(): void
@ -24,8 +26,8 @@ class ServiceProvider extends BaseServiceProvider
$router->patch('mediaupload/{media}', [MediaController::class, 'update'])->name('media.update');
});
MediaCollection::mixin(app(CollectionExtension::class));
// app(CollectionExtension::class)->boot();
}
/**

20
testbench.yaml Normal file
View File

@ -0,0 +1,20 @@
providers:
- Spatie\MediaLibrary\MediaLibraryServiceProvider
- Spatie\LaravelData\LaravelDataServiceProvider
- Zoomyboy\MedialibraryHelper\ServiceProvider
migrations:
- tests/workbench/database/migrations
workbench:
start: '/'
install: false
discovers:
web: false
api: false
commands: false
components: false
views: false
build: []
assets: []
sync: []

View File

@ -0,0 +1,81 @@
<?php
namespace Zoomyboy\MedialibraryHelper\Tests\Feature;
use Illuminate\Support\Facades\Storage;
test('it uploads a deferred file to a collection', function () {
$this->auth()->registerModel()->withoutExceptionHandling();
$content = base64_encode($this->pdfFile()->getContent());
$payload = [
'parent' => ['model' => 'post', 'collection' => 'defaultSingleFile', 'id' => null],
'payload' => [
'content' => $content,
'name' => 'beispiel bild.jpg',
],
];
$this->postJson('/mediaupload', $payload)
->assertStatus(201)
->assertJson([
'is_deferred' => true,
'original_url' => Storage::disk('temp')->url('media-library/beispiel bild.jpg'),
'name' => 'beispiel bild',
'collection_name' => 'defaultSingleFile',
'size' => 3028,
'file_name' => 'beispiel bild.jpg',
'mime_type' => 'application/pdf',
]);
Storage::disk('temp')->assertExists('media-library/beispiel bild.jpg');
});
test('it handles authorization with collection', function () {
$this->auth(['storeMedia' => ['collection' => 'rtrt']])->registerModel();
$content = base64_encode($this->pdfFile()->getContent());
$this->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection' => 'defaultSingleFile', 'id' => null],
'payload' => [
'content' => $content,
'name' => 'beispiel bild.jpg',
],
])->assertStatus(403);
});
test('it handles authorization with collection correctly', function () {
$this->auth(['storeMedia' => ['collection' => 'defaultSingleFile']])->registerModel();
$content = base64_encode($this->pdfFile()->getContent());
$this->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection' => 'defaultSingleFile', 'id' => null],
'payload' => [
'content' => $content,
'name' => 'beispiel bild.jpg',
],
])->assertStatus(201);
});
test('it needs a collection', function ($key, $value) {
$this->auth()->registerModel();
$content = base64_encode($this->pdfFile()->getContent());
$payload = [
'parent' => ['model' => 'post', 'collection' => '', 'id' => null],
'payload' => [
'content' => $content,
'name' => 'beispiel bild.jpg',
],
];
data_set($payload, $key, $value);
$this->postJson('/mediaupload', $payload)->assertJsonValidationErrors($key);
})->with(function () {
yield ['parent.collection', ''];
yield ['parent.collection', -1];
yield ['parent.collection', 'missingcollection'];
yield ['parent.model', 'lalala'];
yield ['parent.model', -1];
yield ['parent.model', ''];
});

View File

@ -3,8 +3,8 @@
namespace Zoomyboy\MedialibraryHelper\Tests\Feature;
use Illuminate\Support\Facades\Event;
use Zoomyboy\MedialibraryHelper\Tests\Events\MediaChange;
use Zoomyboy\MedialibraryHelper\Tests\Events\MediaDestroyed;
use Workbench\App\Events\MediaChange;
use Workbench\App\Events\MediaDestroyed;
test('it deletes multiple media', function () {
$this->auth()->registerModel()->withoutExceptionHandling();

View File

@ -0,0 +1,24 @@
<?php
namespace Zoomyboy\MedialibraryHelper\Tests\Feature;
use Illuminate\Foundation\Console\VendorPublishCommand;
use Illuminate\Support\Facades\Artisan;
use Spatie\MediaLibrary\MediaLibraryServiceProvider;
afterEach(function () {
@unlink(config_path('media-library.php'));
});
test('modifies config file', function () {
Artisan::call(VendorPublishCommand::class, ['--provider' => MediaLibraryServiceProvider::class, '--tag' => 'config']);
$configContents = file_get_contents(config_path('media-library.php'));
$configContents = preg_replace('/\'image_driver\' => env.*/', '\'image_driver\' => "lala",', $configContents);
file_put_contents(config_path('media-library.php'), $configContents);
$this->tearDownTheTestEnvironment();
$this->setUpTheTestEnvironment();
$this->assertEquals('lala', config('media-library.image_driver'));
$this->assertEquals('temp', config('media-library.temp_disk'));
});

View File

@ -3,7 +3,7 @@
namespace Zoomyboy\MedialibraryHelper\Tests\Feature;
use Illuminate\Support\Facades\Event;
use Zoomyboy\MedialibraryHelper\Tests\Events\MediaChange;
use Workbench\App\Events\MediaChange;
test('it can reorder media', function () {
Event::fake();

View File

@ -3,7 +3,7 @@
namespace Zoomyboy\MedialibraryHelper\Tests\Feature;
use Illuminate\Support\Facades\Event;
use Zoomyboy\MedialibraryHelper\Tests\Events\MediaChange;
use Workbench\App\Events\MediaChange;
test('it updates a single files properties', function () {
Event::fake();

View File

@ -4,8 +4,8 @@ namespace Zoomyboy\MedialibraryHelper\Tests\Feature;
use Carbon\Carbon;
use Illuminate\Support\Facades\Event;
use Zoomyboy\MedialibraryHelper\Tests\Events\MediaChange;
use Zoomyboy\MedialibraryHelper\Tests\Events\MediaStored;
use Workbench\App\Events\MediaChange;
use Workbench\App\Events\MediaStored;
test('it uploads a single file to a single file collection', function () {
$this->auth()->registerModel();
@ -13,9 +13,7 @@ test('it uploads a single file to a single file collection', function () {
$content = base64_encode($this->pdfFile()->getContent());
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'defaultSingleFile',
'parent' => ['model' => 'post', 'collection' => 'defaultSingleFile', 'id' => $post->id],
'payload' => [
'content' => $content,
'name' => 'beispiel bild.jpg',
@ -31,92 +29,18 @@ test('it uploads a single file to a single file collection', function () {
$response->assertJsonPath('name', 'beispiel bild');
$response->assertJsonPath('collection_name', 'defaultSingleFile');
$response->assertJsonPath('file_name', 'beispiel-bild.jpg');
$response->assertJsonPath('is_deferred', false);
$response->assertJsonMissingPath('model_type');
$response->assertJsonMissingPath('model_id');
});
test('test validation', function (array $attributes, string $messages) {
$this->auth()->registerModel();
$post = $this->newPost();
$content = base64_encode($this->pdfFile()->getContent());
$this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'defaultSingleFile',
'payload' => [
'content' => $content,
'name' => 'beispiel bild.jpg',
],
...$attributes
])->assertJsonValidationErrors($messages);
})->with([
'missing collection' => [
['collection' => ''],
'collection'
],
'missing id' => [
['id' => ''],
'id'
],
]);
test('test validation for payload', function () {
$this->auth()->registerModel();
$post = $this->newPost();
$this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'defaultSingleFile',
'payload' => [
'content' => '',
'name' => 'beispiel bild.jpg',
],
])->assertJsonValidationErrors('payload.content');
});
test('test validation for name', function () {
$this->auth()->registerModel();
$post = $this->newPost();
$content = base64_encode($this->pdfFile()->getContent());
$this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'defaultSingleFile',
'payload' => [
'content' => $content,
'name' => '',
],
])->assertJsonValidationErrors('payload.name');
});
test('test validation for extension', function () {
$this->auth()->registerModel();
$post = $this->newPost();
$content = base64_encode($this->pdfFile()->getContent());
$this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'defaultSingleFile',
'payload' => [
'content' => $content,
'name' => 'aaa',
],
])->assertJsonValidationErrors('payload.name');
});
test('it uploads a single image to a single file collection', function () {
$this->auth()->registerModel();
$this->auth()->registerModel()->withoutExceptionHandling();
$post = $this->newPost();
$content = base64_encode($this->jpgFile()->getContent());
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'defaultSingleFile',
'parent' => ['model' => 'post', 'collection' => 'defaultSingleFile', 'id' => $post->id],
'payload' => [
'content' => $content,
'name' => 'beispiel bild.jpg',
@ -134,9 +58,7 @@ test('it forces a filename for a single collection', function () {
$content = base64_encode($this->pdfFile()->getContent());
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'singleForced',
'parent' => ['model' => 'post', 'collection' => 'singleForced', 'id' => $post->id],
'payload' => [
'content' => $content,
'name' => 'beispiel bild.jpg',
@ -155,9 +77,7 @@ test('it sets custom title when storing', function () {
$content = base64_encode($this->pdfFile()->getContent());
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'singleStoringHook',
'parent' => ['model' => 'post', 'collection' => 'singleStoringHook', 'id' => $post->id],
'payload' => [
'content' => $content,
'name' => 'beispiel bild.jpg',
@ -177,9 +97,7 @@ test('it sets custom properties from properties method', function () {
$content = base64_encode($this->pdfFile()->getContent());
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'multipleProperties',
'parent' => ['model' => 'post', 'collection' => 'multipleProperties', 'id' => $post->id],
'payload' => [
'content' => $content,
'name' => 'beispiel bild.jpg',
@ -199,9 +117,7 @@ test('it forces a filename for multiple collections', function () {
$content = base64_encode($this->pdfFile()->getContent());
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'multipleForced',
'parent' => ['model' => 'post', 'collection' => 'multipleForced', 'id' => $post->id],
'payload' => [
[
'content' => $content,
@ -222,9 +138,7 @@ test('it throws event when file has been uploaded', function () {
$content = base64_encode($this->pdfFile()->getContent());
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'singleWithEvent',
'parent' => ['model' => 'post', 'collection' => 'singleWithEvent', 'id' => $post->id],
'payload' => [
'content' => $content,
'name' => 'beispiel bild.jpg',
@ -244,9 +158,7 @@ test('it throws event when multiple files uploaded', function () {
$content = base64_encode($this->pdfFile()->getContent());
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'multipleFilesWithEvent',
'parent' => ['model' => 'post', 'collection' => 'multipleFilesWithEvent', 'id' => $post->id],
'payload' => [
[
'content' => $content,
@ -271,9 +183,7 @@ test('it uploads multiple files', function () {
$post->addMedia($file->getPathname())->preservingOriginal()->toMediaCollection('images');
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'images',
'parent' => ['model' => 'post', 'collection' => 'images', 'id' => $post->id],
'payload' => [
[
'content' => base64_encode($file->getContent()),
@ -305,80 +215,125 @@ test('it returns 403 when not authorized', function () {
$this->auth(['storeMedia' => false])->registerModel();
$post = $this->newPost();
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'defaultSingleFile',
$this->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection' => 'defaultSingleFile', 'id' => $post->id],
'payload' => [
'content' => base64_encode($this->pdfFile()->getContent()),
'name' => 'beispiel bild.jpg',
],
]);
$response->assertStatus(403);
])->assertStatus(403);
});
test('it needs validation for single files', function (array $payload, string $invalidFieldName) {
test('it checks for model when running authorization', function () {
$otherPost = $this->newPost();
$post = $this->newPost();
$this->auth(['storeMedia' => ['id' => $post->id, 'collection' => 'defaultSingleFile']])->registerModel();
$this->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection' => 'defaultSingleFile', 'id' => $post->id],
'payload' => [
'content' => base64_encode($this->pdfFile()->getContent()),
'name' => 'beispiel bild.jpg',
],
])->assertStatus(201);
$this->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection' => 'defaultSingleFile', 'id' => $otherPost->id],
'payload' => [
'content' => base64_encode($this->pdfFile()->getContent()),
'name' => 'beispiel bild.jpg',
],
])->assertStatus(403);
});
test('it checks for collection when running authorization', function () {
$post = $this->newPost();
$this->auth(['storeMedia' => ['id' => $post->id, 'collection' => 'defaultSingleFile']])->registerModel();
$this->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection' => 'defaultSingleFile', 'id' => $post->id],
'payload' => [
'content' => base64_encode($this->pdfFile()->getContent()),
'name' => 'beispiel bild.jpg',
],
])->assertStatus(201);
$this->postJson('/mediaupload', [
'parent' => ['model' => 'post', 'collection' => 'singleWithEvent', 'id' => $post->id],
'payload' => [
'content' => base64_encode($this->pdfFile()->getContent()),
'name' => 'beispiel bild.jpg',
],
])->assertStatus(403);
});
test('it needs validation for single files', function (array $payloadOverwrites, string $invalidFieldName) {
$this->auth()->registerModel();
$post = $this->newPost();
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'defaultSingleFile',
$payload = [
'parent' => ['model' => 'post', 'collection' => 'defaultSingleFile', 'id' => $post->id],
'payload' => [
'content' => base64_encode($this->pdfFile()->getContent()),
'name' => 'beispiel bild.jpg',
],
...$payload,
]);
];
foreach ($payloadOverwrites as $key => $value) {
data_set($payload, $key, $value);
}
$response = $this->postJson('/mediaupload', $payload);
$response->assertStatus(422);
$response->assertJsonValidationErrors($invalidFieldName);
})->with(function () {
yield [['model' => 'missingmodel'], 'model'];
yield [['id' => -1], 'model'];
yield [['collection' => 'missingcollection'], 'collection'];
yield [['payload' => ['name' => 'AAA', 'content' => []]], 'payload.content'];
yield [['payload' => ['name' => 'AAA', 'content' => ['UU']]], 'payload.content'];
yield [['payload' => ['name' => 'AAA', 'content' => null]], 'payload.content'];
yield [['payload' => ['name' => 'AAA', 'content' => '']], 'payload.content'];
yield [['payload' => ['name' => 'AAA', 'content' => 1]], 'payload.content'];
yield [['payload' => ['name' => '', 'content' => 'aaadfdf']], 'payload.name'];
yield [['payload' => ['name' => ['U'], 'content' => 'aaadfdf']], 'payload.name'];
yield [['payload' => ['name' => 1, 'content' => 'aaadfdf']], 'payload.name'];
yield [['payload' => ['name' => null, 'content' => 'aaadfdf']], 'payload.name'];
yield [['parent.model' => 'missingmodel'], 'parent.model'];
yield [['parent.id' => -1], 'parent.id'];
yield [['parent.collection' => 'missingcollection'], 'parent.collection'];
yield [['payload.content' => []], 'payload.content'];
yield [['payload.content' => ['UU']], 'payload.content'];
yield [['payload.content' => null], 'payload.content'];
yield [['payload.content' => ''], 'payload.content'];
yield [['payload.content' => 1], 'payload.content'];
yield [['payload.name' => ''], 'payload.name'];
yield [['payload.name' => ['U']], 'payload.name'];
yield [['payload.name' => 1], 'payload.name'];
yield [['payload.name' => null], 'payload.name'];
yield [['payload' => 'lalal'], 'payload'];
yield [['payload' => 55], 'payload'];
});
test('it needs validation for multiple files', function (array $payload, string $invalidFieldName) {
test('it needs validation for multiple files', function (array $payloadOverwrites, string $invalidFieldName) {
$this->auth()->registerModel();
$post = $this->newPost();
$response = $this->postJson('/mediaupload', [
'model' => 'post',
'id' => $post->id,
'collection' => 'images',
$payload = [
'parent' => ['model' => 'post', 'collection' => 'images', 'id' => $post->id],
'payload' => [
[
'content' => base64_encode($this->pdfFile()->getContent()),
'name' => 'beispiel bild.jpg',
],
],
...$payload,
]);
];
foreach ($payloadOverwrites as $key => $value) {
data_set($payload, $key, $value);
}
$response = $this->postJson('/mediaupload', $payload);
$response->assertStatus(422);
$response->assertJsonValidationErrors($invalidFieldName);
})->with(function () {
yield [['model' => 'missingmodel'], 'model'];
yield [['id' => -1], 'model'];
yield [['collection' => 'missingcollection'], 'collection'];
yield [['parent.model' => 'missingmodel'], 'parent.model'];
yield [['parent.model' => 'post.missingcollection'], 'parent.model'];
yield [['parent.id' => -1], 'parent.id'];
yield [['payload' => 'lalal'], 'payload'];
yield [['payload' => []], 'payload'];
yield [['payload' => 1], 'payload'];
yield [['payload' => [['name' => 'AAA', 'content' => []]]], 'payload.0.content'];
yield [['payload' => [['name' => 'AAA', 'content' => ['UU']]]], 'payload.0.content'];
yield [['payload' => [['name' => 'AAA', 'content' => null]]], 'payload.0.content'];

View File

@ -1,7 +1,11 @@
<?php
use Illuminate\Auth\Access\Gate;
use Illuminate\Support\Facades\Storage;
use Workbench\App\Models\Post;
use Workbench\App\Policies\PostPolicy;
uses(Zoomyboy\MedialibraryHelper\Tests\TestCase::class)->in('Feature');
uses(Illuminate\Foundation\Testing\RefreshDatabase::class)->in('Feature');
uses()->beforeEach(fn () => Storage::fake('media'))->in('Feature');
uses()->beforeEach(fn () => Storage::fake('media') && Storage::fake('temp'))->in('Feature');
uses()->beforeEach(fn () => Gate::policy(Post::class, PostPolicy::class))->in('Feature');

View File

@ -4,30 +4,15 @@ namespace Zoomyboy\MedialibraryHelper\Tests;
use Illuminate\Http\File;
use Illuminate\Support\Facades\Gate;
use Orchestra\Testbench\Concerns\WithWorkbench;
use Orchestra\Testbench\TestCase as BaseTestCase;
use Spatie\LaravelData\LaravelDataServiceProvider;
use Spatie\MediaLibrary\MediaLibraryServiceProvider;
use Zoomyboy\MedialibraryHelper\ServiceProvider;
use Zoomyboy\MedialibraryHelper\Tests\Models\Post;
use Workbench\App\Models\Post;
use Workbench\App\Models\User;
class TestCase extends BaseTestCase
{
/**
* Define database migrations.
*/
protected function defineDatabaseMigrations(): void
{
$this->loadMigrationsFrom(__DIR__ . '/migrations');
}
protected function getPackageProviders($app): array
{
return [
ServiceProvider::class,
MediaLibraryServiceProvider::class,
LaravelDataServiceProvider::class,
];
}
use WithWorkbench;
/**
* Generate a pdf file with a filename and get path.
@ -65,23 +50,8 @@ class TestCase extends BaseTestCase
protected function auth(array $policies = []): self
{
$policies = [
'storeMedia' => true,
'updateMedia' => true,
'destroyMedia' => true,
'listMedia' => true,
...$policies,
];
foreach ($policies as $ability => $result) {
Gate::define($ability, fn (?string $user, string $collectionName) => $result);
}
$this->be(User::factory()->policies($policies)->create());
return $this;
}
protected function defineEnvironment($app)
{
$app['config']->set('media-library.middleware', ['web']);
}
}

View File

@ -1,6 +1,6 @@
<?php
namespace Zoomyboy\MedialibraryHelper\Tests\Events;
namespace Workbench\App\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

View File

@ -1,6 +1,6 @@
<?php
namespace Zoomyboy\MedialibraryHelper\Tests\Events;
namespace Workbench\App\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

View File

@ -1,6 +1,6 @@
<?php
namespace Zoomyboy\MedialibraryHelper\Tests\Events;
namespace Workbench\App\Events;
use Illuminate\Foundation\Events\Dispatchable;
use Illuminate\Queue\SerializesModels;

View File

@ -1,6 +1,6 @@
<?php
namespace Zoomyboy\MedialibraryHelper\Tests\Models;
namespace Workbench\App\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Event;
@ -8,9 +8,9 @@ use Illuminate\Support\Str;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Zoomyboy\MedialibraryHelper\Tests\Events\MediaChange;
use Zoomyboy\MedialibraryHelper\Tests\Events\MediaDestroyed;
use Zoomyboy\MedialibraryHelper\Tests\Events\MediaStored;
use Workbench\App\Events\MediaChange;
use Workbench\App\Events\MediaDestroyed;
use Workbench\App\Events\MediaStored;
class Post extends Model implements HasMedia
{

View File

@ -0,0 +1,17 @@
<?php
namespace Workbench\App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
class User extends Authenticatable
{
use HasFactory;
public $guarded = [];
public $casts = [
'policies' => 'json',
];
}

View File

@ -0,0 +1,56 @@
<?php
namespace Workbench\App\Policies;
use Illuminate\Auth\Access\HandlesAuthorization;
use Spatie\MediaLibrary\HasMedia;
use Workbench\App\Models\User;
class PostPolicy
{
use HandlesAuthorization;
public function listMedia(User $user, HasMedia $model): bool
{
if (is_bool($user->policies['listMedia'])) {
return $user->policies['listMedia'] === true;
}
return data_get($user->policies, 'listMedia.id') === $model->id
&& data_get($user->policies, 'listMedia.collection') === $collection;
}
public function storeMedia(User $user, ?HasMedia $model, ?string $collection = null): bool
{
if (is_bool($user->policies['storeMedia'])) {
return $user->policies['storeMedia'] === true;
}
if (is_null($model)) {
return data_get($user->policies, 'storeMedia.collection') === $collection;
}
return data_get($user->policies, 'storeMedia.id') === $model->id
&& data_get($user->policies, 'storeMedia.collection') === $collection;
}
public function updateMedia(User $user, HasMedia $model, string $collection): bool
{
if (is_bool($user->policies['updateMedia'])) {
return $user->policies['updateMedia'] === true;
}
return data_get($user->policies, 'updateMedia.id') === $model->id
&& data_get($user->policies, 'updateMedia.collection') === $collection;
}
public function destroyMedia(User $user, HasMedia $model, string $collection): bool
{
if (is_bool($user->policies['destroyMedia'])) {
return $user->policies['destroyMedia'] === true;
}
return data_get($user->policies, 'destroyMedia.id') === $model->id
&& data_get($user->policies, 'destroyMedia.collection') === $collection;
}
}

View File

@ -0,0 +1,41 @@
<?php
namespace Database\Factories\Models;
use Workbench\App\Models\User;
use Illuminate\Database\Eloquent\Factories\Factory;
use Illuminate\Support\Facades\Hash;
/**
* @extends Factory<User>
*/
class UserFactory extends Factory
{
protected $model = User::class;
/**
* Define the model's default state.
*
* @return array<string, mixed>
*/
public function definition()
{
return [
'email' => $this->faker->safeEmail,
'password' => Hash::make('password'),
'name' => $this->faker->firstName,
'policies' => [],
];
}
public function policies(array $policies): self
{
return $this->state(['policies' => [
'storeMedia' => true,
'updateMedia' => true,
'destroyMedia' => true,
'listMedia' => true,
...$policies,
]]);
}
}

View File

@ -0,0 +1,20 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
public function up(): void
{
Schema::create('users', function (Blueprint $table) {
$table->id();
$table->string('email');
$table->string('name');
$table->string('password');
$table->json('policies');
$table->timestamps();
});
}
};