diff --git a/.github/workflows/run-pint.yml b/.github/workflows/run-pint.yml index 35157a0..212170d 100644 --- a/.github/workflows/run-pint.yml +++ b/.github/workflows/run-pint.yml @@ -5,6 +5,10 @@ on: paths: - "**.php" +permissions: + contents: write + pull-requests: write + jobs: pint-code-style: runs-on: ubuntu-latest diff --git a/.gitignore b/.gitignore index 6e49a00..827219d 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,6 @@ /.vagrant .phpunit.result.cache .DS_Store -composer.lock \ No newline at end of file +composer.lock +/storage/logs/* +!/storage/logs/.gitkeep diff --git a/app/Commands/InstallBrowserCommand.php b/app/Commands/InstallBrowserCommand.php new file mode 100644 index 0000000..6ad46ce --- /dev/null +++ b/app/Commands/InstallBrowserCommand.php @@ -0,0 +1,25 @@ + 'linux64', + 'mac-arm' => 'mac-arm64', + 'mac-intel' => 'mac-x64', + 'win' => 'win64', + ]; + + protected function configure(): void + { + $this->addOption( + 'ver', + null, + InputOption::VALUE_OPTIONAL, + 'Install a specific version', + '115.0.5763.0', + ); + + $this->addOption( + 'latest', + null, + InputOption::VALUE_NONE, + 'Install the latest version', + ); + + $this->addOption( + 'path', + null, + InputOption::VALUE_OPTIONAL, + 'Specify the path where to download it', + ); + } + + public function handle(): int + { + if (empty($downloadable = $this->version())) { + error("There' no versions available for [{$this->option('ver')}]"); + + return self::FAILURE; + } + + $os = OperatingSystem::id(); + + $version = $downloadable->getVersion(); + + try { + spin( + callback: fn () => $downloadable->download(GoogleDownloadable::BROWSER, $this->getDownloadDirectory(), $this->platforms[$os], true), + message: "Downloading Google Chrome {$this->getComponentName()} [$version]" + ); + + $this->message("Google Chrome {$this->getComponentName()} unzip it on [{$this->getDownloadDirectory()}]", 'info'); + } catch (\Throwable $e) { + Log::error($e->getMessage()); + + error("Unable to download/install Google Chrome {$this->getComponentName()} [$version]"); + + return self::FAILURE; + } + + $this->message("Google Chrome {$this->getComponentName()} [$version] downloaded", 'success'); + + return self::SUCCESS; + } + + protected function version(): ?GoogleDownloadable + { + if ($this->option('latest')) { + return GoogleForTesting::getLatestVersion(); + } + + $version = $this->option('ver'); + + $downloadable = spin( + callback: fn () => GoogleForTesting::getVersion($version), + message: "Searching for version [$version]" + ); + + if (filled($downloadable)) { + return $downloadable; + } + + $versions = GoogleForTesting::getMilestone(Str::before($version, '.')); + + if (empty($versions)) { + return null; + } + + warning("There isn't an exact version [$version]"); + + $version = search( + label: 'We found similar versions, please choose one', + options: fn () => $versions->mapWithKeys(fn ($d) => [$d->getVersion() => $d->getVersion()])->all(), + placeholder: 'Choose your prefer version' + ); + + return GoogleForTesting::getVersion($version); + } + + protected function getBasePath(string $path = null): string + { + $folder = join_paths(getenv('HOME'), '.google-for-testing'); + + File::ensureDirectoryExists($folder); + + return join_paths($folder, $path ?? ''); + } + + public function message(string $text, string $type = 'line'): void + { + $color = match ($type) { + 'success' => 'bg-green', + 'warning' => 'bg-yellow', + 'error' => 'bg-red', + 'info' => 'bg-blue', + default => 'bg-gray-600', + }; + + $type = str($type)->upper(); + + render(<< + $type + + $text +

+ HTML); + } + + protected function getDownloadDirectory(): string + { + return $this->option('path') ?? $this->getBasePath(); + } + + protected function getComponent(): int + { + return $this->component; + } + + protected function getComponentName(): string + { + return $this->getComponent() === GoogleDownloadable::BROWSER ? 'Browser' : 'Driver'; + } +} diff --git a/app/Commands/InstallDriverCommand.php b/app/Commands/InstallDriverCommand.php new file mode 100644 index 0000000..e4204e3 --- /dev/null +++ b/app/Commands/InstallDriverCommand.php @@ -0,0 +1,19 @@ + getMilestone(string $version) Get a collection with all the versions available for a Milestone + */ +class GoogleForTesting extends Facade +{ + public static function getFacadeAccessor(): string + { + return 'gft'; + } +} diff --git a/app/GoogleDownloadable.php b/app/GoogleDownloadable.php new file mode 100644 index 0000000..95c7f6e --- /dev/null +++ b/app/GoogleDownloadable.php @@ -0,0 +1,97 @@ +version; + } + + public function getMilestone(): string + { + return Str::of($this->version)->before('.'); + } + + /** + * @throws \RuntimeException if the required platform doesn't exist + */ + public function getChromeBrowserURL(string $platform): string + { + $item = collect($this->browserDownloads)->first(fn (array $item) => $item['platform'] === $platform); + + if (empty($item)) { + throw new \RuntimeException("The URL for Google Chrome Browser for platform [$platform], it's not available"); + } + + return $item['url']; + } + + /** + * @throws \RuntimeException if the required platform doesn't exist + */ + public function getChromeDriverURL(string $platform): string + { + $item = collect($this->driverDownloads)->first(fn (array $item) => $item['platform'] === $platform); + + if (empty($item)) { + throw new \RuntimeException("The URL for Google Chrome Driver for platform [$platform], it's not available"); + } + + return $item['url']; + } + + public function download(int $component, string $to, string $platform, bool $unzip = false): void + { + if ($component & static::BROWSER) { + $url = $this->getChromeBrowserURL($platform); + $filename = join_paths($to, Str::afterLast($url, '/')); + + download($url, $filename); + + $unzip && unzip($filename); + } + + if ($component & static::DRIVER) { + $url = $this->getChromeDriverURL($platform); + $filename = join_paths($to, Str::afterLast($url, '/')); + + download($url, $filename); + + $unzip && unzip($filename); + } + } + + public static function make(string $version, string $revision, array $browserDownloads, array $driverDownloads): static + { + return new static($version, $revision, $browserDownloads, $driverDownloads); + } + + public static function makeFromArray(array $data): static + { + $downloads = $data['downloads']; + + $version = $data['version']; + $revision = $data['revision']; + $browserDownloads = $downloads['chrome']; + $driverDownloads = $downloads['chromedriver'] ?? []; + + return static::make($version, $revision, $browserDownloads, $driverDownloads); + } +} diff --git a/app/GoogleForTesting.php b/app/GoogleForTesting.php new file mode 100644 index 0000000..91ff6ef --- /dev/null +++ b/app/GoogleForTesting.php @@ -0,0 +1,58 @@ +json('channels')['Stable']; + + $version = $channel['version']; + + return static::getVersion($version); + } + + public function getVersion(string $version): ?GoogleDownloadable + { + $response = Http::get(static::$downloads); + + $exact = collect($response->json('versions')) + ->first(fn (array $item) => $item['version'] == $version); + + if (empty($exact)) { + return null; + } + + return GoogleDownloadable::makeFromArray($exact); + } + + public function getMilestone(string $milestone): ?Collection + { + $response = Http::get(static::$downloads); + + $versions = collect($response->json('versions')) + ->filter(fn (array $item) => Str::before($item['version'], '.') == $milestone) + ->map(fn (array $version) => GoogleDownloadable::makeFromArray($version)); + + if ($versions->isEmpty()) { + return null; + } + + return $versions; + } +} diff --git a/app/OperatingSystem.php b/app/OperatingSystem.php new file mode 100644 index 0000000..05530b1 --- /dev/null +++ b/app/OperatingSystem.php @@ -0,0 +1,47 @@ + 'mac-arm', + default => 'mac-intel', + }; + } +} diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php index 4f1cfae..d43fe12 100644 --- a/app/Providers/AppServiceProvider.php +++ b/app/Providers/AppServiceProvider.php @@ -2,6 +2,7 @@ namespace App\Providers; +use App\GoogleForTesting; use Illuminate\Support\ServiceProvider; class AppServiceProvider extends ServiceProvider @@ -11,7 +12,9 @@ class AppServiceProvider extends ServiceProvider */ public function boot(): void { - // + $this->app->bind('gft', function () { + return new GoogleForTesting; + }); } /** diff --git a/app/helpers.php b/app/helpers.php index 6a05e94..56fbb3c 100644 --- a/app/helpers.php +++ b/app/helpers.php @@ -16,7 +16,7 @@ function join_paths(string ...$paths): string array_map(fn (string $p) => trim($p, DIRECTORY_SEPARATOR), $paths) ); - return join(DIRECTORY_SEPARATOR, [rtrim($first, DIRECTORY_SEPARATOR), ...$paths]); + return implode(DIRECTORY_SEPARATOR, [rtrim($first, DIRECTORY_SEPARATOR), ...$paths]); } } @@ -36,9 +36,7 @@ function download(string $url, string $file, bool $force = false): void File::delete($file); - $encoded_url = urlencode($url); - - $response = Http::get($encoded_url, ['stream' => true])->toPsrResponse(); + $response = Http::withOptions(['stream' => true])->get($url)->toPsrResponse(); $body = $response->getBody(); @@ -63,9 +61,8 @@ function download(string $url, string $file, bool $force = false): void * This function will attempt to unzip the given zip file name into the given location, but if the location * is not provided, we'll use the file directory. * - * @param string $filename The file name of the ZIP file - * @param ?string $to The location where to extract the content - * @return void + * @param string $filename The file name of the ZIP file + * @param ?string $to The location where to extract the content */ function unzip(string $filename, string $to = null): void { diff --git a/composer.json b/composer.json index aa170b6..a8c13f5 100644 --- a/composer.json +++ b/composer.json @@ -20,13 +20,16 @@ "php": "^8.1", "guzzlehttp/guzzle": "^7.5", "illuminate/http": "^10.0", + "illuminate/log": "^10.0", "laravel-zero/framework": "^10.0.2", "nunomaduro/termwind": "^1.15.1" }, "require-dev": { "laravel/pint": "^1.8", "mockery/mockery": "^1.5.1", - "pestphp/pest": "^2.5" + "pestphp/pest": "^2.5", + "pestphp/pest-plugin-laravel": "^2.2", + "spatie/laravel-ray": "^1.33" }, "autoload": { "psr-4": { diff --git a/config/logging.php b/config/logging.php new file mode 100644 index 0000000..6720dfa --- /dev/null +++ b/config/logging.php @@ -0,0 +1,118 @@ + env('LOG_CHANNEL', 'stack'), + + /* + |-------------------------------------------------------------------------- + | Deprecations Log Channel + |-------------------------------------------------------------------------- + | + | This option controls the log channel that should be used to log warnings + | regarding deprecated PHP and library features. This allows you to get + | your application ready for upcoming major versions of dependencies. + | + */ + + 'deprecations' => env('LOG_DEPRECATIONS_CHANNEL', 'null'), + + /* + |-------------------------------------------------------------------------- + | Log Channels + |-------------------------------------------------------------------------- + | + | Here you may configure the log channels for your application. Out of + | the box, Laravel uses the Monolog PHP logging library. This gives + | you a variety of powerful log handlers / formatters to utilize. + | + | Available Drivers: "single", "daily", "slack", "syslog", + | "errorlog", "monolog", + | "custom", "stack" + | + */ + + 'channels' => [ + 'stack' => [ + 'driver' => 'stack', + 'channels' => ['stderr'], + 'ignore_exceptions' => false, + ], + + 'single' => [ + 'driver' => 'single', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + ], + + 'daily' => [ + 'driver' => 'daily', + 'path' => storage_path('logs/laravel.log'), + 'level' => env('LOG_LEVEL', 'debug'), + 'days' => 14, + ], + + 'slack' => [ + 'driver' => 'slack', + 'url' => env('LOG_SLACK_WEBHOOK_URL'), + 'username' => 'Laravel Log', + 'emoji' => ':boom:', + 'level' => env('LOG_LEVEL', 'critical'), + ], + + 'papertrail' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => SyslogUdpHandler::class, + 'handler_with' => [ + 'host' => env('PAPERTRAIL_URL'), + 'port' => env('PAPERTRAIL_PORT'), + ], + ], + + 'stderr' => [ + 'driver' => 'monolog', + 'level' => env('LOG_LEVEL', 'debug'), + 'handler' => StreamHandler::class, + 'formatter' => env('LOG_STDERR_FORMATTER'), + 'with' => [ + 'stream' => 'php://stderr', + ], + ], + + 'syslog' => [ + 'driver' => 'syslog', + 'level' => env('LOG_LEVEL', 'debug'), + ], + + 'errorlog' => [ + 'driver' => 'errorlog', + 'level' => env('LOG_LEVEL', 'debug'), + ], + + 'null' => [ + 'driver' => 'monolog', + 'handler' => NullHandler::class, + ], + + 'emergency' => [ + 'path' => storage_path('logs/laravel.log'), + ], + ], + +]; diff --git a/app/Commands/.gitkeep b/storage/logs/.gitkeep similarity index 100% rename from app/Commands/.gitkeep rename to storage/logs/.gitkeep diff --git a/tests/Feature/.gitkeep b/tests/Feature/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/tests/Feature/InstallBrowserCommandTest.php b/tests/Feature/InstallBrowserCommandTest.php index 5981820..1d4c412 100644 --- a/tests/Feature/InstallBrowserCommandTest.php +++ b/tests/Feature/InstallBrowserCommandTest.php @@ -1,21 +1,123 @@ shouldReceive('getVersion') + ->andReturn('115.0.5763.0'); + + $downloadable->shouldReceive('download'); + + $google->shouldReceive('getVersion') + ->andReturn($downloadable); + + artisan('install:browser') + ->doesntExpectOutputToContain("There' no versions available for [115.0.5763.0]") + ->expectsOutputToContain('Downloading Google Chrome Browser [115.0.5763.0]') + ->expectsOutputToContain('Google Chrome Browser [115.0.5763.0] downloaded') + ->expectsOutputToContain('Google Chrome Browser unzip it on') + ->assertSuccessful(); +}); + it('download the latest browser version', function () { + Http::fake(); + + $google = GoogleForTesting::partialMock(); + $downloadable = Mockery::mock(GoogleDownloadable::class); + + $downloadable->shouldReceive('getVersion') + ->andReturn('200.0.0.0'); + + $downloadable->shouldReceive('download'); + + $downloadable->shouldReceive('getChromeBrowserURL') + ->andReturn('https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/200.0.0.0/linux64/chrome-linux64.zip'); -})->todo(); + $google->shouldReceive('getLatestVersion') + ->andReturn($downloadable); -it('it download the browser version []', function () { + artisan('install:browser --latest') + ->doesntExpectOutputToContain("There' no versions available for [200.0.0.0]") + ->expectsOutputToContain('Downloading Google Chrome Browser [200.0.0.0]') + ->expectsOutputToContain('Google Chrome Browser [200.0.0.0] downloaded') + ->assertSuccessful(); +}); -})->todo(); +it('it download the browser version [113.0.5672.0]', function () { + Http::fake(); + + $google = GoogleForTesting::partialMock(); + $downloadable = Mockery::mock(GoogleDownloadable::class); + + $downloadable->shouldReceive('getVersion') + ->andReturn('113.0.5672.0'); + + $downloadable->shouldReceive('download'); + + $google->shouldReceive('getVersion') + ->andReturn($downloadable); + + artisan('install:browser --ver=113.0.5672.0') + ->doesntExpectOutputToContain("There' no versions available for [113.0.5672.0]") + ->expectsOutputToContain('Downloading Google Chrome Browser [113.0.5672.0]') + ->expectsOutputToContain('Google Chrome Browser [113.0.5672.0] downloaded') + ->assertSuccessful(); +}); it('download the browser on other path', function () { + Http::fake(); + + $google = GoogleForTesting::partialMock(); + $downloadable = Mockery::mock(GoogleDownloadable::class); -})->todo(); + $downloadable->shouldReceive('getVersion') + ->andReturn('200.0.0.0'); + + $downloadable->shouldReceive('download'); + + $google->shouldReceive('getLatestVersion') + ->andReturn($downloadable); + + artisan('install:browser --latest --path=/some/dir/to/download') + ->doesntExpectOutputToContain("There' no versions available for [200.0.0.0]") + ->expectsOutputToContain('Downloading Google Chrome Browser [200.0.0.0]') + ->expectsOutputToContain('Google Chrome Browser [200.0.0.0] downloaded') + ->expectsOutputToContain('Google Chrome Browser unzip it on [/some/dir/to/download]') + ->assertSuccessful(); +}); it('try to download a pre-existing browser version', function () { + Http::fake(); + + Log::partialMock() + ->shouldReceive('error') + ->with('The file [chrome-linux.zip] already exists'); + + $google = GoogleForTesting::partialMock(); + $downloadable = Mockery::mock(GoogleDownloadable::class); + + $downloadable + ->shouldReceive('getVersion') + ->andReturn('115.0.5763.0'); -})->todo(); + $downloadable + ->shouldReceive('download') + ->andThrow(\Exception::class, 'The file [chrome-linux.zip] already exists'); -it('try to download a non existing browser version', function () { + $google->shouldReceive('getVersion') + ->andReturn($downloadable); -})->todo(); + artisan('install:browser') + ->doesntExpectOutputToContain('Google Chrome Browser [115.0.5763.0] downloaded') + ->assertFailed(); +}); diff --git a/tests/Feature/InstallDriverCommandTest.php b/tests/Feature/InstallDriverCommandTest.php new file mode 100644 index 0000000..11bcca7 --- /dev/null +++ b/tests/Feature/InstallDriverCommandTest.php @@ -0,0 +1,123 @@ +shouldReceive('getVersion') + ->andReturn('115.0.5763.0'); + + $downloadable->shouldReceive('download'); + + $google->shouldReceive('getVersion') + ->andReturn($downloadable); + + artisan('install:driver') + ->doesntExpectOutputToContain("There' no versions available for [115.0.5763.0]") + ->expectsOutputToContain('Downloading Google Chrome Driver [115.0.5763.0]') + ->expectsOutputToContain('Google Chrome Driver [115.0.5763.0] downloaded') + ->expectsOutputToContain('Google Chrome Driver unzip it on') + ->assertSuccessful(); +}); + +it('download the latest driver version', function () { + Http::fake(); + + $google = GoogleForTesting::partialMock(); + $downloadable = Mockery::mock(GoogleDownloadable::class); + + $downloadable->shouldReceive('getVersion') + ->andReturn('200.0.0.0'); + + $downloadable->shouldReceive('download'); + + $downloadable->shouldReceive('getChromeBrowserURL') + ->andReturn('https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/200.0.0.0/linux64/chrome-linux64.zip'); + + $google->shouldReceive('getLatestVersion') + ->andReturn($downloadable); + + artisan('install:driver --latest') + ->doesntExpectOutputToContain("There' no versions available for [200.0.0.0]") + ->expectsOutputToContain('Downloading Google Chrome Driver [200.0.0.0]') + ->expectsOutputToContain('Google Chrome Driver [200.0.0.0] downloaded') + ->assertSuccessful(); +}); + +it('it download the driver version [118.0.5672.0]', function () { + Http::fake(); + + $google = GoogleForTesting::partialMock(); + $downloadable = Mockery::mock(GoogleDownloadable::class); + + $downloadable->shouldReceive('getVersion') + ->andReturn('118.0.5672.0'); + + $downloadable->shouldReceive('download'); + + $google->shouldReceive('getVersion') + ->andReturn($downloadable); + + artisan('install:driver --ver=118.0.5672.0') + ->doesntExpectOutputToContain("There' no versions available for [118.0.5672.0]") + ->expectsOutputToContain('Downloading Google Chrome Driver [118.0.5672.0]') + ->expectsOutputToContain('Google Chrome Driver [118.0.5672.0] downloaded') + ->assertSuccessful(); +}); + +it('download the driver on other path', function () { + Http::fake(); + + $google = GoogleForTesting::partialMock(); + $downloadable = Mockery::mock(GoogleDownloadable::class); + + $downloadable->shouldReceive('getVersion') + ->andReturn('200.0.0.0'); + + $downloadable->shouldReceive('download'); + + $google->shouldReceive('getLatestVersion') + ->andReturn($downloadable); + + artisan('install:driver --latest --path=/some/dir/to/download') + ->doesntExpectOutputToContain("There' no versions available for [200.0.0.0]") + ->expectsOutputToContain('Downloading Google Chrome Driver [200.0.0.0]') + ->expectsOutputToContain('Google Chrome Driver [200.0.0.0] downloaded') + ->expectsOutputToContain('Google Chrome Driver unzip it on [/some/dir/to/download]') + ->assertSuccessful(); +}); + +it('try to download a pre-existing driver version', function () { + Http::fake(); + + Log::partialMock() + ->shouldReceive('error') + ->with('The file [chromedriver-linux.zip] already exists'); + + $google = GoogleForTesting::partialMock(); + $downloadable = Mockery::mock(GoogleDownloadable::class); + + $downloadable + ->shouldReceive('getVersion') + ->andReturn('115.0.5763.0'); + + $downloadable + ->shouldReceive('download') + ->andThrow(\Exception::class, 'The file [chromedriver-linux.zip] already exists'); + + $google->shouldReceive('getVersion') + ->andReturn($downloadable); + + artisan('install:driver') + ->doesntExpectOutputToContain('Google Chrome Driver [115.0.5763.0] downloaded') + ->assertFailed(); +}); diff --git a/tests/Unit/HelpersTest.php b/tests/Unit/HelpersTest.php index b6957d3..9443fac 100644 --- a/tests/Unit/HelpersTest.php +++ b/tests/Unit/HelpersTest.php @@ -6,16 +6,16 @@ dataset('paths', fn () => [ 'simple path' => [ 'paths' => ['/this', 'is', 'a', 'path/'], - 'result' => '/this/is/a/path' + 'result' => '/this/is/a/path', ], 'weird path' => [ 'paths' => ['/just/another/', '/path/to/', '/join//'], - 'result' => '/just/another/path/to/join' + 'result' => '/just/another/path/to/join', ], 'strange path' => [ 'paths' => ['what', '//is//this/', '//path//'], - 'result' => 'what/is//this/path' - ] + 'result' => 'what/is//this/path', + ], ]); afterAll(fn () => File::delete(join_paths(__DIR__, '..', 'files', 'file.txt'))); @@ -26,26 +26,26 @@ })->with('paths'); it('download a file', function () { - Http::fake(); + Http::fake(); - $fileMock = File::partialMock(); + $fileMock = File::partialMock(); - $fileMock - ->shouldReceive('exists') - ->andReturn(false, true); + $fileMock + ->shouldReceive('exists') + ->andReturn(false, true); - $fileMock - ->shouldReceive('delete') - ->andReturn(false); + $fileMock + ->shouldReceive('delete') + ->andReturn(false); - $fileMock - ->expects('append') - ->andReturn(); + $fileMock + ->expects('append') + ->andReturn(); - expect(fn () => download('https://fake-download.com', '/path/to/a/file.zip')) - ->not->toThrow(\Exception::class) - ->and(File::exists('/path/to/a/file.zip')) - ->toBeTrue(); + expect(fn () => download('https://fake-download.com', '/path/to/a/file.zip')) + ->not->toThrow(\Exception::class) + ->and(File::exists('/path/to/a/file.zip')) + ->toBeTrue(); }); it('try to download a file that already exists', function () { @@ -61,7 +61,6 @@ ->toThrow(\Exception::class); }); - it('delete a pre-existing file to download it again', function () { Http::fake(); @@ -95,6 +94,3 @@ ->and(File::exists($file)) ->toBeTrue(); }); - - -