├── CHANGELOG.md ├── LICENSE.md ├── README.md ├── composer.json └── src ├── EnvironmentConfig.php └── UsesMinIOServer.php /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to `laravel-minio-testing-tools` will be documented in this file. 4 | 5 | ## 1.0.1 - 2022-05-17 6 | 7 | - Cleanup + bugfix 8 | 9 | ## 1.0.9 - 2022-05-17 10 | 11 | - First release -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) protonemedia 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel MinIO Testing Tools 2 | 3 | [![Latest Version on Packagist](https://img.shields.io/packagist/v/protonemedia/laravel-minio-testing-tools.svg?style=flat-square)](https://packagist.org/packages/protonemedia/laravel-minio-testing-tools) 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE.md) 5 | [![GitHub Tests Action Status](https://img.shields.io/github/workflow/status/protonemedia/laravel-minio-testing-tools/run-tests?label=tests)](https://github.com/protonemedia/laravel-minio-testing-tools/actions?query=workflow%3Arun-tests+branch%3Amain) 6 | [![Total Downloads](https://img.shields.io/packagist/dt/protonemedia/laravel-minio-testing-tools.svg?style=flat-square)](https://packagist.org/packages/protonemedia/laravel-minio-testing-tools) 7 | [![Buy us a tree](https://img.shields.io/badge/Treeware-%F0%9F%8C%B3-lightgreen)](https://plant.treeware.earth/protonemedia/laravel-minio-testing-tools) 8 | 9 | This package provides a trait to run your tests against a MinIO S3 server. 10 | 11 | 📝 Blog post: https://protone.media/en/blog/how-to-use-a-local-minio-s3-server-with-laravel-and-automatically-configure-it-for-your-laravel-dusk-test-suite 12 | 13 | ## Sponsor Us 14 | 15 | [](https://inertiaui.com/inertia-table?utm_source=github&utm_campaign=laravel-minio-testing-tools) 16 | 17 | ❤️ We proudly support the community by developing Laravel packages and giving them away for free. If this package saves you time or if you're relying on it professionally, please consider [sponsoring the maintenance and development](https://github.com/sponsors/pascalbaljet) and check out our latest premium package: [Inertia Table](https://inertiaui.com/inertia-table?utm_source=github&utm_campaign=laravel-minio-testing-tools). Keeping track of issues and pull requests takes time, but we're happy to help! 18 | 19 | ## Features 20 | * Starts and configures a MinIO server for your tests. 21 | * Updates the `filesystems` disk configuration. 22 | * Updates and restores the `.env` file. 23 | * Works with [Laravel Dusk](https://laravel.com/docs/9.x/dusk). 24 | * Works on [GitHub Actions](#github-actions) 25 | * Compatible with Laravel 10. 26 | * PHP 8.2 or higher is required. 27 | 28 | ## Installation 29 | 30 | Make sure you've downloaded the [MinIO Server and Client](https://min.io/download#/linux) for your OS. 31 | 32 | You can install the package via composer: 33 | 34 | ```bash 35 | composer require protonemedia/laravel-minio-testing-tools --dev 36 | ``` 37 | 38 | Add the trait to your test, and add the `bootUsesMinIOServer` method: 39 | 40 | ```php 41 | bootUsesMinIOServer(); 59 | } 60 | 61 | /** @test */ 62 | public function it_can_upload_a_video_using_multipart_upload() 63 | { 64 | } 65 | } 66 | ``` 67 | 68 | That's it! 69 | 70 | ## GitHub Actions 71 | 72 | The easiest way is to download the MinIO Server and Client before the tests are run: 73 | 74 | ```yaml 75 | jobs: 76 | test: 77 | steps: 78 | - uses: actions/checkout@v2 79 | 80 | - name: Download MinIO S3 server and client 81 | run: | 82 | wget https://dl.minio.io/server/minio/release/linux-amd64/minio -q -P /usr/local/bin/ 83 | wget https://dl.minio.io/client/mc/release/linux-amd64/mc -q -P /usr/local/bin/ 84 | chmod +x /usr/local/bin/minio 85 | chmod +x /usr/local/bin/mc 86 | minio --version 87 | mc --version 88 | ``` 89 | 90 | If you're using `php artisan serve`, make sure you don't use the `--no-reload` flag, as the `.env` file will be changed on-the-fly. 91 | 92 | Optionally, if you want persistent storage across the test suite, you may start the server manually before the tests are run. 93 | 94 | ```yaml 95 | jobs: 96 | test: 97 | steps: 98 | - uses: actions/checkout@v2 99 | 100 | - name: Download MinIO S3 server and client 101 | run: | 102 | wget https://dl.minio.io/server/minio/release/linux-amd64/minio -q -P /usr/local/bin/ 103 | wget https://dl.minio.io/client/mc/release/linux-amd64/mc -q -P /usr/local/bin/ 104 | chmod +x /usr/local/bin/minio 105 | chmod +x /usr/local/bin/mc 106 | minio --version 107 | mc --version 108 | 109 | - name: Run MinIO S3 server 110 | run: | 111 | mkdir ~/s3 112 | sudo minio server ~/s3 --json > minio-log.json & 113 | 114 | - name: Configure MinIO S3 115 | run: | 116 | mc config host add local http://127.0.0.1:9000 minioadmin minioadmin 117 | mc admin user add local user password 118 | mc admin policy set local readwrite user=user 119 | mc mb local/bucket-name --region=eu-west-1 120 | 121 | - name: Upload Minio Logs (optional) 122 | if: failure() 123 | uses: actions/upload-artifact@v2 124 | with: 125 | name: minio 126 | path: minio-log.json 127 | ``` 128 | 129 | In this case, you also need to supply an environment file with the MinIO configuration. This makes the configuration also accessible by the browser session when you're running Laravel Dusk. 130 | 131 | ```env 132 | AWS_ACCESS_KEY_ID=user 133 | AWS_SECRET_ACCESS_KEY=password 134 | AWS_DEFAULT_REGION=eu-west-1 135 | AWS_BUCKET=bucket-name 136 | AWS_URL=http://127.0.0.1:9000 137 | AWS_ENDPOINT=http://127.0.0.1:9000 138 | AWS_USE_PATH_STYLE_ENDPOINT=true 139 | ``` 140 | 141 | ## Testing 142 | 143 | ```bash 144 | composer test 145 | ``` 146 | 147 | ## Changelog 148 | 149 | Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently. 150 | 151 | ## Contributing 152 | 153 | Please see [CONTRIBUTING](CONTRIBUTING.md) for details. 154 | 155 | ## Other Laravel packages 156 | 157 | * [`Inertia Table`](https://inertiaui.com/inertia-table?utm_source=github&utm_campaign=laravel-minio-testing-tools): The Ultimate Table for Inertia.js with built-in Query Builder. 158 | * [`Laravel Blade On Demand`](https://github.com/protonemedia/laravel-blade-on-demand): Laravel package to compile Blade templates in memory. 159 | * [`Laravel Cross Eloquent Search`](https://github.com/protonemedia/laravel-cross-eloquent-search): Laravel package to search through multiple Eloquent models. 160 | * [`Laravel Eloquent Scope as Select`](https://github.com/protonemedia/laravel-eloquent-scope-as-select): Stop duplicating your Eloquent query scopes and constraints in PHP. This package lets you re-use your query scopes and constraints by adding them as a subquery. 161 | * [`Laravel FFMpeg`](https://github.com/protonemedia/laravel-ffmpeg): This package provides an integration with FFmpeg for Laravel. The storage of the files is handled by Laravel's Filesystem. 162 | * [`Laravel MinIO Testing Tools`](https://github.com/protonemedia/laravel-minio-testing-tools): Run your tests against a MinIO S3 server. 163 | * [`Laravel Mixins`](https://github.com/protonemedia/laravel-mixins): A collection of Laravel goodies. 164 | * [`Laravel Paddle`](https://github.com/protonemedia/laravel-paddle): Paddle.com API integration for Laravel with support for webhooks/events. 165 | * [`Laravel Task Runner`](https://github.com/protonemedia/laravel-task-runner): Write Shell scripts like Blade Components and run them locally or on a remote server. 166 | * [`Laravel Verify New Email`](https://github.com/protonemedia/laravel-verify-new-email): This package adds support for verifying new email addresses: when a user updates its email address, it won't replace the old one until the new one is verified. 167 | * [`Laravel XSS Protection`](https://github.com/protonemedia/laravel-xss-protection): Laravel Middleware to protect your app against Cross-site scripting (XSS). It sanitizes request input, and it can sanatize Blade echo statements. 168 | 169 | ## Security 170 | 171 | If you discover any security-related issues, please email code@protone.media instead of using the issue tracker. Please do not email any questions, open an issue if you have a question. 172 | 173 | ## Credits 174 | 175 | - [Pascal Baljet](https://github.com/pascalbaljet) 176 | - [All Contributors](../../contributors) 177 | 178 | ## License 179 | 180 | The MIT License (MIT). Please see [License File](LICENSE.md) for more information. 181 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "protonemedia/laravel-minio-testing-tools", 3 | "description": "This is my package laravel-minio-testing-tools", 4 | "keywords": [ 5 | "protonemedia", 6 | "laravel", 7 | "laravel-minio-testing-tools" 8 | ], 9 | "homepage": "https://github.com/protonemedia/laravel-minio-testing-tools", 10 | "license": "MIT", 11 | "authors": [ 12 | { 13 | "name": "Pascal Baljet", 14 | "email": "pascal@protone.media", 15 | "role": "Developer" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.2|^8.3|^8.4", 20 | "guzzlehttp/guzzle": "^7.4", 21 | "illuminate/contracts": "^11.43|^12.0", 22 | "spatie/laravel-package-tools": "^1.9.2", 23 | "spatie/temporary-directory": "^2.1" 24 | }, 25 | "require-dev": { 26 | "nunomaduro/collision": "^7.0|^8.0", 27 | "orchestra/testbench": "^9.0|^10.0", 28 | "pestphp/pest": "^3.0", 29 | "pestphp/pest-plugin-laravel": "^3.0", 30 | "phpunit/phpunit": "^10.4|^11.5.3" 31 | }, 32 | "autoload": { 33 | "psr-4": { 34 | "ProtoneMedia\\LaravelMinioTestingTools\\": "src", 35 | "ProtoneMedia\\LaravelMinioTestingTools\\Database\\Factories\\": "database/factories" 36 | } 37 | }, 38 | "autoload-dev": { 39 | "psr-4": { 40 | "ProtoneMedia\\LaravelMinioTestingTools\\Tests\\": "tests" 41 | } 42 | }, 43 | "scripts": { 44 | "analyse": "vendor/bin/phpstan analyse", 45 | "test": "vendor/bin/pest", 46 | "test-coverage": "vendor/bin/pest --coverage" 47 | }, 48 | "config": { 49 | "sort-packages": true, 50 | "allow-plugins": { 51 | "pestphp/pest-plugin": true 52 | } 53 | }, 54 | "minimum-stability": "dev", 55 | "prefer-stable": true 56 | } -------------------------------------------------------------------------------- /src/EnvironmentConfig.php: -------------------------------------------------------------------------------- 1 | minioValue)) { 18 | return $this->minioValue ? 'true' : 'false'; 19 | } 20 | 21 | return $this->minioValue; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/UsesMinIOServer.php: -------------------------------------------------------------------------------- 1 | startMinIOServer(); 27 | $this->configureMinIO(); 28 | } 29 | 30 | public function initMinIOConfigCollection() 31 | { 32 | $this->minIODiskConfig = collect([ 33 | new EnvironmentConfig('key', 'AWS_ACCESS_KEY_ID'), 34 | new EnvironmentConfig('secret', 'AWS_SECRET_ACCESS_KEY'), 35 | new EnvironmentConfig('region', 'AWS_DEFAULT_REGION'), 36 | new EnvironmentConfig('bucket', 'AWS_BUCKET'), 37 | new EnvironmentConfig('url', 'AWS_URL'), 38 | new EnvironmentConfig('endpoint', 'AWS_ENDPOINT'), 39 | new EnvironmentConfig('use_path_style_endpoint', 'AWS_USE_PATH_STYLE_ENDPOINT'), 40 | ])->keyBy->configKey; 41 | } 42 | 43 | /** 44 | * Extracts the port from the 'endpoint' configuration key. 45 | * 46 | * @return integer|null 47 | */ 48 | public function getMinIOPortFromConfig(): ?int 49 | { 50 | $url = config("filesystems.disks.{$this->minIODisk}.endpoint"); 51 | 52 | return parse_url($url, PHP_URL_PORT) ?: null; 53 | } 54 | 55 | /** 56 | * Finds a free port to run the MinIO server on. 57 | * 58 | * @return integer 59 | */ 60 | public function findFreePort(): int 61 | { 62 | $sock = socket_create_listen(0); 63 | socket_getsockname($sock, $addr, $port); 64 | socket_close($sock); 65 | 66 | return $port; 67 | } 68 | 69 | /** 70 | * Returns a boolean whether the server has started. 71 | * 72 | * @return boolean 73 | */ 74 | public function minIOServerHasStarted(): bool 75 | { 76 | if (!$this->minIOPort) { 77 | return false; 78 | } 79 | 80 | return rescue( 81 | fn () => Http::timeout(1) 82 | ->connectTimeout(1) 83 | ->get("http://127.0.0.1:{$this->minIOPort}/minio/health/live") 84 | ->ok(), 85 | false, 86 | false 87 | ) === true; 88 | } 89 | 90 | /** 91 | * Runs a command with a default timeout, and returns the output. 92 | * 93 | * @param string $cmd 94 | * @param integer $timeout 95 | * @return string 96 | */ 97 | private function exec(string $cmd, int $timeout = 10): string 98 | { 99 | $process = Process::fromShellCommandline($cmd)->setTimeout($timeout); 100 | $process->run(); 101 | 102 | return trim($process->getOutput()); 103 | } 104 | 105 | /** 106 | * Verifies if a server is already running, for example in CI, or boots up 107 | * a new MinIO server and waits for it to be available. 108 | * 109 | * @return bool 110 | */ 111 | public function startMinIOServer(): bool 112 | { 113 | $this->minIOPort = $this->getMinIOPortFromConfig(); 114 | 115 | if ($this->minIOServerHasStarted()) { 116 | return true; 117 | } 118 | 119 | $this->minIOPort = $this->findFreePort(); 120 | 121 | $temporaryDirectory = TemporaryDirectory::make( 122 | storage_path('framework/testing/minio') 123 | ); 124 | 125 | $pid = $this->exec("minio server {$temporaryDirectory->path()} --address :{$this->minIOPort} > /dev/null 2>&1 & echo $!"); 126 | 127 | $killMinIOAndDeleteStorage = function () use ($pid, $temporaryDirectory) { 128 | if ($this->minIODestroyed) { 129 | return; 130 | } 131 | 132 | if ($pid) { 133 | $this->exec("kill {$pid}"); 134 | } 135 | 136 | // deleting the directory might fail when minio is not killed yet 137 | rescue(fn () => $temporaryDirectory->delete()); 138 | 139 | $this->minIODestroyed = true; 140 | }; 141 | 142 | $this->beforeApplicationDestroyed($killMinIOAndDeleteStorage); 143 | 144 | register_shutdown_function($killMinIOAndDeleteStorage); 145 | 146 | $tries = 0; 147 | 148 | while (!$this->minIOServerHasStarted()) { 149 | usleep(1000); 150 | $tries++; 151 | 152 | if ($tries === 10 * 1000) { 153 | $this->fail("Could not start MinIO server."); 154 | } 155 | } 156 | 157 | return true; 158 | } 159 | 160 | /** 161 | * Configures MinIO with the disk key, secret, region and bucket. Then it 162 | * updates the configuration to use the correct endpoint and URL. 163 | * 164 | * @return void 165 | */ 166 | public function configureMinIO() 167 | { 168 | $url = "http://127.0.0.1:{$this->minIOPort}"; 169 | 170 | $username = config("filesystems.disks.{$this->minIODisk}.key") ?: 'user'; 171 | $password = config("filesystems.disks.{$this->minIODisk}.secret") ?: 'password'; 172 | $region = config("filesystems.disks.{$this->minIODisk}.region") ?: 'eu-west-1'; 173 | $bucket = config("filesystems.disks.{$this->minIODisk}.bucket") ?: 'bucket-name'; 174 | 175 | $addLocalMinIO = $this->exec("until (mc config host add local {$url} minioadmin minioadmin) do echo '...waiting...' && sleep 1; done;"); 176 | 177 | if (!Str::contains($addLocalMinIO, 'Added `local` successfully.')) { 178 | $this->fail('Could not configure MinIO server'); 179 | } 180 | 181 | $this->exec("mc admin user add local {$username} {$password}"); 182 | $this->exec("mc admin policy set local readwrite user={$username}"); 183 | $this->exec("mc mb local/{$bucket} --region={$region}"); 184 | 185 | $this->initMinIOConfigCollection(); 186 | 187 | $this->minIODiskConfig->get('key')->minioValue = $username; 188 | $this->minIODiskConfig->get('secret')->minioValue = $password; 189 | $this->minIODiskConfig->get('region')->minioValue = $region; 190 | $this->minIODiskConfig->get('bucket')->minioValue = $bucket; 191 | $this->minIODiskConfig->get('endpoint')->minioValue = $url; 192 | $this->minIODiskConfig->get('url')->minioValue = $url; 193 | $this->minIODiskConfig->get('use_path_style_endpoint')->minioValue = true; 194 | 195 | $this->minIODiskConfig->each(function (EnvironmentConfig $config) { 196 | config()->set("filesystems.disks.{$this->minIODisk}.{$config->configKey}", $config->minioValue); 197 | }); 198 | 199 | $this->updateEnvirionmentFile(); 200 | } 201 | 202 | /** 203 | * This updates the environment file so the settings also 204 | * apply to browser sessions with Laravel Dusk. 205 | * 206 | * @return void 207 | */ 208 | public function updateEnvirionmentFile() 209 | { 210 | if (file_exists(base_path('.env.dusk')) && !file_exists(base_path('.env.backup'))) { 211 | throw new Exception("No environment backup file."); 212 | } 213 | 214 | $envFilename = base_path('.env'); 215 | 216 | $env = file_get_contents($envFilename); 217 | 218 | $this->minIODiskConfig->each(function (EnvironmentConfig $config) use (&$env) { 219 | // backup current line 220 | preg_match("^({$config->environmentKey}=)(.)*^", $env, $matches); 221 | $config->environmentBackupLine = $matches[0] ?? ''; 222 | 223 | // replace value 224 | $env = preg_replace( 225 | "^({$config->environmentKey}=)(.)*^", 226 | "{$config->environmentKey}={$config->castMinioValue()}", 227 | $env 228 | ); 229 | }); 230 | 231 | file_put_contents($envFilename, $env); 232 | 233 | $this->beforeApplicationDestroyed(fn () => $this->restoreEnvironmentFile()); 234 | 235 | register_shutdown_function(fn () => $this->restoreEnvironmentFile()); 236 | } 237 | 238 | /** 239 | * Restores the original values in the environment file 240 | * before 'updateEnvirionmentFile' ran. 241 | * 242 | * @return void 243 | */ 244 | public function restoreEnvironmentFile() 245 | { 246 | if ($this->minIOEnvironmentRestored) { 247 | return; 248 | } 249 | 250 | $envFilename = base_path('.env'); 251 | 252 | if (!file_exists($envFilename)) { 253 | return; 254 | } 255 | 256 | $env = file_get_contents($envFilename); 257 | 258 | $this->minIODiskConfig->each(function (EnvironmentConfig $config) use (&$env) { 259 | $env = preg_replace( 260 | "^({$config->environmentKey}=)(.)*^", 261 | $config->environmentBackupLine, 262 | $env 263 | ); 264 | }); 265 | 266 | file_put_contents($envFilename, $env); 267 | 268 | $this->minIOEnvironmentRestored = true; 269 | } 270 | } 271 | --------------------------------------------------------------------------------