From 2af8681883b72f2536d6f8f697060f525cd0e90f Mon Sep 17 00:00:00 2001 From: Eduar Bastidas Date: Sun, 21 Jul 2024 14:41:07 -0400 Subject: [PATCH] wip --- .github/workflows/run-tests.yml | 2 +- composer.json | 8 +- phpunit.xml.dist | 7 +- routes/web.php | 6 + ...orageMultipartUploadControllerContract.php | 17 +++ .../Controllers/S3MultipartController.php | 136 ++++++++++++++++++ .../CompleteMultipartUploadRequest.php | 30 ++++ .../Requests/CreateMultipartUploadRequest.php | 31 ++++ src/Http/Requests/SignPartRequest.php | 30 ++++ src/LaravelS3MultipartServiceProvider.php | 7 + tests/{ => Common}/ArchTest.php | 0 .../Unit/BladeFunctionGeneratorTest.php | 2 +- tests/Isolated/S3MultipartControllerTest.php | 79 ++++++++++ tests/TestCase.php | 11 +- 14 files changed, 355 insertions(+), 11 deletions(-) create mode 100644 routes/web.php create mode 100644 src/Contracts/StorageMultipartUploadControllerContract.php create mode 100644 src/Http/Controllers/S3MultipartController.php create mode 100644 src/Http/Requests/CompleteMultipartUploadRequest.php create mode 100644 src/Http/Requests/CreateMultipartUploadRequest.php create mode 100644 src/Http/Requests/SignPartRequest.php rename tests/{ => Common}/ArchTest.php (100%) rename tests/{ => Common}/Unit/BladeFunctionGeneratorTest.php (86%) create mode 100644 tests/Isolated/S3MultipartControllerTest.php diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 8d3b5f7..758b47b 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -55,4 +55,4 @@ jobs: run: composer show -D - name: Execute tests - run: vendor/bin/pest --ci + run: vendor/bin/pest --ci --testsuite=Common && vendor/bin/pest --ci --testsuite=Isolated diff --git a/composer.json b/composer.json index 1e164ee..81317f5 100644 --- a/composer.json +++ b/composer.json @@ -17,13 +17,15 @@ ], "require": { "php": "^8.1", - "spatie/laravel-package-tools": "^1.16", - "illuminate/contracts": "^10.0||^11.0" + "aws/aws-sdk-php": "^3.316", + "illuminate/contracts": "^10.0||^11.0", + "spatie/laravel-package-tools": "^1.16" }, "require-dev": { + "larastan/larastan": "^2.9", "laravel/pint": "^1.14", + "mockery/mockery": "^1.6", "nunomaduro/collision": "^8.1.1||^7.10.0", - "larastan/larastan": "^2.9", "orchestra/testbench": "^9.0.0||^8.22.0", "pestphp/pest": "^2.34", "pestphp/pest-plugin-arch": "^2.7", diff --git a/phpunit.xml.dist b/phpunit.xml.dist index ec29126..835bd7e 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -16,8 +16,11 @@ backupStaticProperties="false" > - - tests + + ./tests/Common + + + ./tests/Isolated diff --git a/routes/web.php b/routes/web.php new file mode 100644 index 0000000..d9da66c --- /dev/null +++ b/routes/web.php @@ -0,0 +1,6 @@ +name('s3m.create-multipart'); diff --git a/src/Contracts/StorageMultipartUploadControllerContract.php b/src/Contracts/StorageMultipartUploadControllerContract.php new file mode 100644 index 0000000..16340cd --- /dev/null +++ b/src/Contracts/StorageMultipartUploadControllerContract.php @@ -0,0 +1,17 @@ +ensureEnvironmentVariablesAreAvailable($request); + + $client = $this->storageClient(); + + $bucket = $request->input('bucket') ?: $_ENV['AWS_BUCKET']; + + $uuid = (string) Str::uuid(); + + $key = $this->getKey($uuid); + + try { + $uploader = $client->createMultipartUpload([ + 'Bucket' => $bucket, + 'Key' => $key, + 'ACL' => $request->input('visibility') ?: $this->defaultVisibility(), + 'ContentType' => $request->input('content_type') ?: 'application/octet-stream', + ]); + + return response()->json([ + 'uuid' => $uuid, + 'bucket' => $bucket, + 'key' => $key, + 'uploadId' => $uploader['UploadId'], + ]); + } catch (Exception $e) { + return response()->json([ + 'error' => $e->getMessage(), + ], 500); + } + } + + /** + * Sign a part upload. + */ + public function signPartUpload(SignPartRequest $request): JsonResponse + { + return new JsonResponse([]); + } + + /** + * Complete a multipart upload. + */ + public function completeMultipartUpload(Request $request): JsonResponse + { + return new JsonResponse([]); + } + + /** + * Ensure the required environment variables are available. + * + * @throws \InvalidArgumentException + */ + protected function ensureEnvironmentVariablesAreAvailable(Request $request): void + { + $missing = array_diff_key(array_flip(array_filter([ + $request->input('bucket') ? null : 'AWS_BUCKET', + 'AWS_DEFAULT_REGION', + 'AWS_ACCESS_KEY_ID', + 'AWS_SECRET_ACCESS_KEY', + ])), $_ENV); + + if (empty($missing)) { + return; + } + + throw new InvalidArgumentException( + 'Unable to issue signed URL. Missing environment variables: '.implode(', ', array_keys($missing)) + ); + } + + /** + * Get the S3 storage client instance. + */ + protected function storageClient(): S3Client + { + $config = [ + 'region' => config('filesystems.disks.s3.region', $_ENV['AWS_DEFAULT_REGION']), + 'version' => 'latest', + 'signature_version' => 'v4', + 'use_path_style_endpoint' => config('filesystems.disks.s3.use_path_style_endpoint', false), + ]; + + $config['credentials'] = array_filter([ + 'key' => $_ENV['AWS_ACCESS_KEY_ID'] ?? null, + 'secret' => $_ENV['AWS_SECRET_ACCESS_KEY'] ?? null, + 'token' => $_ENV['AWS_SESSION_TOKEN'] ?? null, + 'url' => $_ENV['AWS_URL'] ?? null, + 'endpoint' => $_ENV['AWS_URL'] ?? null, + ]); + + if (array_key_exists('AWS_URL', $_ENV) && ! is_null($_ENV['AWS_URL'])) { + $config['url'] = $_ENV['AWS_URL']; + $config['endpoint'] = $_ENV['AWS_URL']; + } + + return new S3Client($config); + } + + /** + * Get key for the given UUID. + */ + protected function getKey(string $uuid): string + { + return 'tmp/'.$uuid; + } + + /** + * Get the default visibility for uploads. + */ + protected function defaultVisibility(): string + { + return 'private'; + } +} diff --git a/src/Http/Requests/CompleteMultipartUploadRequest.php b/src/Http/Requests/CompleteMultipartUploadRequest.php new file mode 100644 index 0000000..7f18aaf --- /dev/null +++ b/src/Http/Requests/CompleteMultipartUploadRequest.php @@ -0,0 +1,30 @@ +user(), $this->input('bucket')]); + } + + /** + * Get the validation rules that apply to the request. + * + * @return array + */ + public function rules(): array + { + return [ + 'bucket' => ['nullable', 'string'], + 'visibility' => ['nullable', 'string'], + 'content_type' => ['nullable', 'string'], + ]; + } +} diff --git a/src/Http/Requests/SignPartRequest.php b/src/Http/Requests/SignPartRequest.php new file mode 100644 index 0000000..a0618fe --- /dev/null +++ b/src/Http/Requests/SignPartRequest.php @@ -0,0 +1,30 @@ +app->singleton( + Contracts\StorageMultipartUploadControllerContract::class, + S3MultipartController::class + ); + if ($this->app->resolved('blade.compiler')) { $this->registerDirective($this->app['blade.compiler']); } else { @@ -18,6 +24,7 @@ public function configurePackage(Package $package): void $package ->name('laravel-s3-multipart') + ->hasRoute('web') ->hasConfigFile(); } diff --git a/tests/ArchTest.php b/tests/Common/ArchTest.php similarity index 100% rename from tests/ArchTest.php rename to tests/Common/ArchTest.php diff --git a/tests/Unit/BladeFunctionGeneratorTest.php b/tests/Common/Unit/BladeFunctionGeneratorTest.php similarity index 86% rename from tests/Unit/BladeFunctionGeneratorTest.php rename to tests/Common/Unit/BladeFunctionGeneratorTest.php index a8a7dd3..f7fcede 100644 --- a/tests/Unit/BladeFunctionGeneratorTest.php +++ b/tests/Common/Unit/BladeFunctionGeneratorTest.php @@ -3,7 +3,7 @@ use MrEduar\LaravelS3Multipart\BladeFunctionGenerator; test('render script tag', function () { - $routeFunction = file_get_contents(__DIR__.'/../../dist/function.umd.js'); + $routeFunction = file_get_contents(__DIR__.'/../../../dist/function.umd.js'); expect((new BladeFunctionGenerator)->generate())->toBe( << $_ENV['AWS_BUCKET'] = 'storage', + 'filesystems.disks.s3.key' => $_ENV['AWS_ACCESS_KEY_ID'] = 'key', + 'filesystems.disks.s3.region' => $_ENV['AWS_DEFAULT_REGION'] = 'us-east-1', + 'filesystems.disks.s3.secret' => $_ENV['AWS_SECRET_ACCESS_KEY'] = 'password', + 'filesystems.disks.s3.url' => $_ENV['AWS_URL'] = 'http://minio:9000', + 'filesystems.disks.s3.use_path_style_endpoint' => true, + ]); + + Gate::define('uploadFiles', static function ($user = null, $bucket = null): bool { + return true; + }); +}); + +afterEach(function () { + Mockery::close(); +}); + +test('response contains a upload id', function () { + $mock = Mockery::mock('overload:'.Aws\S3\S3Client::class); + + $mock->shouldReceive('createMultipartUpload')->once()->andReturn([ + 'UploadId' => 'example-upload-id', + ]); + + $this->app->instance(Aws\S3\S3Client::class, $mock); + + getJson(route('s3m.create-multipart')) + ->assertStatus(200) + ->assertJson(fn (AssertableJson $json) => $json + ->has('uuid') + ->has('bucket') + ->has('key') + ->has('uploadId') + ->etc() + ); +}); + +it('data are validating', function () { + $mock = Mockery::mock('overload:'.Aws\S3\S3Client::class); + + $mock->shouldReceive('createMultipartUpload')->once()->andReturn([ + 'UploadId' => 'example-upload-id', + ]); + + $this->app->instance(Aws\S3\S3Client::class, $mock); + + getJson(route('s3m.create-multipart', [ + 'bucket' => 'test-bucket', + 'visibility' => 'public', + 'content_type' => 'image/jpeg', + 'cache_control' => 'max-age=31536000', + ]))->assertOk() + ->assertJson(fn (AssertableJson $json) => $json + ->has('uuid') + ->has('key') + ->where('bucket', 'test-bucket') + ->where('uploadId', 'example-upload-id') + ->etc() + ); + + getJson(route('s3m.create-multipart', [ + 'bucket' => [ + 'test-bucket', + ], + 'visibility' => 'public', + 'content_type' => 'image/jpeg', + 'cache_control' => 'max-age=31536000', + ]))->assertInvalid('bucket'); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index 96b708f..ff30e84 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,7 +2,7 @@ namespace MrEduar\LaravelS3Multipart\Tests; -use Illuminate\Database\Eloquent\Factories\Factory; +use Mockery; use MrEduar\LaravelS3Multipart\LaravelS3MultipartServiceProvider; use Orchestra\Testbench\TestCase as Orchestra; @@ -11,10 +11,13 @@ class TestCase extends Orchestra protected function setUp(): void { parent::setUp(); + } + + protected function tearDown(): void + { + Mockery::close(); - Factory::guessFactoryNamesUsing( - fn (string $modelName) => 'MrEduar\\LaravelS3Multipart\\Database\\Factories\\'.class_basename($modelName).'Factory' - ); + parent::tearDown(); } protected function getPackageProviders($app)