diff --git a/bdi b/bdi index 79f1084..d636fab 100755 --- a/bdi +++ b/bdi @@ -49,8 +49,10 @@ $geckoDriverVersionResolver = new GeckoDriver\VersionResolver($httpClient); $driverVersionResolverFactory->register($chromeDriverVersionResolver); $driverVersionResolverFactory->register($geckoDriverVersionResolver); +$chromeDriverDownloadUrlResolver = new ChromeDriver\DownloadUrlResolver($httpClient); + $driverDownloaderFactory = new Driver\DownloaderFactory(); -$driverDownloaderFactory->register(new ChromeDriver\Downloader($filesystem, $httpClient, $multiExtractor)); +$driverDownloaderFactory->register(new ChromeDriver\Downloader($filesystem, $httpClient, $multiExtractor, $chromeDriverDownloadUrlResolver)); $driverDownloaderFactory->register(new GeckoDriver\Downloader($filesystem, $httpClient, $multiExtractor)); $browserFactory = new Browser\BrowserFactory($browserPathResolverFactory, $browserVersionResolverFactory); diff --git a/src/Driver/ChromeDriver/DownloadUrlResolver.php b/src/Driver/ChromeDriver/DownloadUrlResolver.php new file mode 100644 index 0000000..48b49db --- /dev/null +++ b/src/Driver/ChromeDriver/DownloadUrlResolver.php @@ -0,0 +1,106 @@ +httpClient = $httpClient; + } + + public function byDriver(Driver $driver): string + { + if (! VersionResolver::isJsonVersion($driver->version())) { + return sprintf( + '%s/%s/%s.zip', + self::LEGACY_DOWNLOAD_ENDPOINT, + $driver->version()->toBuildString(), + $this->getBinaryName($driver), + ); + } + + $response = $this->httpClient->request('GET', self::LATEST_PATCH_WITH_DOWNLOAD_ENDPOINT); + + $versions = $response->toArray(); + if (! isset($versions['builds'][$driver->version()->toString()]['downloads']['chromedriver'])) { + throw new UnexpectedValueException(sprintf('Could not find the chromedriver downloads for version %s', $driver->version()->toString())); + } + + $platformName = $this->getPlatformName($driver); + $downloads = $versions['builds'][$driver->version()->toString()]['downloads']['chromedriver']; + foreach ($downloads as $download) { + if ($download['platform'] === $platformName && isset($download['url']) && is_string($download['url'])) { + return $download['url']; + } + } + + $operatingSystem = $driver->operatingSystem(); + + throw new UnexpectedValueException(sprintf( + 'Could not resolve chromedriver download url for version %s with binary %s', + $driver->version()->toString(), + $operatingSystem->getValue() + )); + } + + private function getBinaryName(Driver $driver): string + { + $operatingSystem = $driver->operatingSystem(); + if ($operatingSystem->equals(OperatingSystem::WINDOWS())) { + return self::BINARY_WINDOWS; + } + + if ($operatingSystem->equals(OperatingSystem::MACOS())) { + return self::BINARY_MAC; + } + + if ($operatingSystem->equals(OperatingSystem::LINUX())) { + return self::BINARY_LINUX; + } + + throw NotImplemented::feature( + sprintf('Downloading %s for %s', $driver->name()->getValue(), $operatingSystem->getValue()) + ); + } + + private function getPlatformName(Driver $driver): string + { + $operatingSystem = $driver->operatingSystem(); + if ($operatingSystem->equals(OperatingSystem::WINDOWS())) { + return 'win32'; + } + + if ($operatingSystem->equals(OperatingSystem::MACOS())) { + return 'mac-x64'; + } + + if ($operatingSystem->equals(OperatingSystem::LINUX())) { + return 'linux64'; + } + + throw NotImplemented::feature( + sprintf('Downloading %s for %s', $driver->name()->getValue(), $operatingSystem->getValue()) + ); + } +} diff --git a/src/Driver/ChromeDriver/Downloader.php b/src/Driver/ChromeDriver/Downloader.php index d283be6..f420e47 100644 --- a/src/Driver/ChromeDriver/Downloader.php +++ b/src/Driver/ChromeDriver/Downloader.php @@ -6,6 +6,7 @@ use DBrekelmans\BrowserDriverInstaller\Archive\Extractor; use DBrekelmans\BrowserDriverInstaller\Driver\Downloader as DownloaderInterface; +use DBrekelmans\BrowserDriverInstaller\Driver\DownloadUrlResolver; use DBrekelmans\BrowserDriverInstaller\Driver\Driver; use DBrekelmans\BrowserDriverInstaller\Driver\DriverName; use DBrekelmans\BrowserDriverInstaller\Exception\NotImplemented; @@ -31,17 +32,9 @@ final class Downloader implements DownloaderInterface { - private const DOWNLOAD_ENDPOINT = 'https://chromedriver.storage.googleapis.com'; - private const BINARY_LINUX = 'chromedriver_linux64'; - private const BINARY_MAC = 'chromedriver_mac64'; - private const BINARY_WINDOWS = 'chromedriver_win32'; - private const DOWNLOAD_ENDPOINT_JSON = 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing'; - private const DOWNLOAD_ENDPOINT_JSON_NEW = 'https://storage.googleapis.com/chrome-for-testing-public'; - private const BINARY_LINUX_JSON = 'chromedriver-linux64'; - private const BINARY_MAC_JSON = 'chromedriver-mac-x64'; - private const BINARY_WINDOWS_JSON = 'chromedriver-win32'; - private const NEW_JSON_API_ENDPOINT_MAJOR = 121; - private const NEW_JSON_API_ENDPOINT_PATCH = 6167; + private const BINARY_LINUX_JSON = 'chromedriver-linux64'; + private const BINARY_MAC_JSON = 'chromedriver-mac-x64'; + private const BINARY_WINDOWS_JSON = 'chromedriver-win32'; private Filesystem $filesystem; @@ -52,12 +45,19 @@ final class Downloader implements DownloaderInterface private string $tempDir; - public function __construct(Filesystem $filesystem, HttpClientInterface $httpClient, Extractor $archiveExtractor) - { - $this->filesystem = $filesystem; - $this->httpClient = $httpClient; - $this->archiveExtractor = $archiveExtractor; - $this->tempDir = sys_get_temp_dir(); + private DownloadUrlResolver $downloadUrlResolver; + + public function __construct( + Filesystem $filesystem, + HttpClientInterface $httpClient, + Extractor $archiveExtractor, + DownloadUrlResolver $downloadUrlResolver + ) { + $this->filesystem = $filesystem; + $this->httpClient = $httpClient; + $this->archiveExtractor = $archiveExtractor; + $this->downloadUrlResolver = $downloadUrlResolver; + $this->tempDir = sys_get_temp_dir(); } public function supports(Driver $driver): bool @@ -124,12 +124,7 @@ private function downloadArchive(Driver $driver): string $response = $this->httpClient->request( 'GET', - sprintf( - '%s/%s/%s.zip', - $this->getDownloadEndpoint($driver), - $driver->version()->toBuildString(), - $this->getBinaryName($driver) - ) + $this->downloadUrlResolver->byDriver($driver), ); $fileHandler = fopen($temporaryFile, 'wb'); @@ -147,43 +142,6 @@ private function downloadArchive(Driver $driver): string return $temporaryFile; } - /** - * @throws NotImplemented - */ - private function getBinaryName(Driver $driver): string - { - $operatingSystem = $driver->operatingSystem(); - if ($this->isJsonVersion($driver)) { - if ($operatingSystem->equals(OperatingSystem::WINDOWS())) { - return 'win32/' . self::BINARY_WINDOWS_JSON; - } - - if ($operatingSystem->equals(OperatingSystem::MACOS())) { - return 'mac-x64/' . self::BINARY_MAC_JSON; - } - - if ($operatingSystem->equals(OperatingSystem::LINUX())) { - return 'linux64/' . self::BINARY_LINUX_JSON; - } - } else { - if ($operatingSystem->equals(OperatingSystem::WINDOWS())) { - return self::BINARY_WINDOWS; - } - - if ($operatingSystem->equals(OperatingSystem::MACOS())) { - return self::BINARY_MAC; - } - - if ($operatingSystem->equals(OperatingSystem::LINUX())) { - return self::BINARY_LINUX; - } - } - - throw NotImplemented::feature( - sprintf('Downloading %s for %s', $driver->name()->getValue(), $operatingSystem->getValue()) - ); - } - /** * @throws RuntimeException * @throws IOException @@ -192,7 +150,7 @@ private function extractArchive(string $archive, Driver $driver): string { $unzipLocation = $this->tempDir . DIRECTORY_SEPARATOR . 'chromedriver'; $extractedFiles = $this->archiveExtractor->extract($archive, $unzipLocation); - if ($this->isJsonVersion($driver)) { + if (VersionResolver::isJsonVersion($driver->version())) { $extractedFiles = $this->cleanArchiveStructure($driver, $unzipLocation, $extractedFiles); } @@ -234,34 +192,6 @@ private function getFileName(OperatingSystem $operatingSystem): string return $fileName; } - private function isJsonVersion(Driver $driver): bool - { - return $driver->version()->major() >= VersionResolver::MAJOR_VERSION_ENDPOINT_BREAKPOINT; - } - - private function getDownloadEndpoint(Driver $driver): string - { - if ($this->isJsonVersion($driver)) { - return $this->resolveJsonVersionEndpoint($driver); - } - - return self::DOWNLOAD_ENDPOINT; - } - - private function resolveJsonVersionEndpoint(Driver $driver): string - { - $version = $driver->version(); - if ((int) $version->major() < self::NEW_JSON_API_ENDPOINT_MAJOR) { - return self::DOWNLOAD_ENDPOINT_JSON; - } - - if ((int) $version->major() === self::NEW_JSON_API_ENDPOINT_MAJOR && (int) $version->patch() < self::NEW_JSON_API_ENDPOINT_PATCH) { - return self::DOWNLOAD_ENDPOINT_JSON; - } - - return self::DOWNLOAD_ENDPOINT_JSON_NEW; - } - /** * @param string[] $extractedFiles * diff --git a/src/Driver/ChromeDriver/VersionResolver.php b/src/Driver/ChromeDriver/VersionResolver.php index 92f0461..a84f2f0 100644 --- a/src/Driver/ChromeDriver/VersionResolver.php +++ b/src/Driver/ChromeDriver/VersionResolver.php @@ -28,6 +28,11 @@ final class VersionResolver implements VersionResolverInterface private HttpClientInterface $httpClient; + public static function isJsonVersion(Version $version): bool + { + return $version->major() >= self::MAJOR_VERSION_ENDPOINT_BREAKPOINT; + } + public function __construct(HttpClientInterface $httpClient) { $this->httpClient = $httpClient; diff --git a/src/Driver/DownloadUrlResolver.php b/src/Driver/DownloadUrlResolver.php new file mode 100644 index 0000000..7d08ada --- /dev/null +++ b/src/Driver/DownloadUrlResolver.php @@ -0,0 +1,10 @@ + + */ + public static function byDriverDataProvider(): iterable + { + yield 'legacy version linux' => [ + new Driver(DriverName::CHROME(), Version::fromString('88.0.4299.0'), OperatingSystem::LINUX()), + 'https://chromedriver.storage.googleapis.com/88.0.4299.0/chromedriver_linux64.zip', + ]; + + yield 'legacy version macos' => [ + new Driver(DriverName::CHROME(), Version::fromString('88.0.4299.0'), OperatingSystem::MACOS()), + 'https://chromedriver.storage.googleapis.com/88.0.4299.0/chromedriver_mac64.zip', + ]; + + yield 'legacy version windows' => [ + new Driver(DriverName::CHROME(), Version::fromString('88.0.4299.0'), OperatingSystem::WINDOWS()), + 'https://chromedriver.storage.googleapis.com/88.0.4299.0/chromedriver_win32.zip', + ]; + + yield 'new version linux' => [ + new Driver(DriverName::CHROME(), Version::fromString('115.0.5790.170'), OperatingSystem::LINUX()), + 'https://dynamic-url-2/', + ]; + + yield 'new version macos' => [ + new Driver(DriverName::CHROME(), Version::fromString('115.0.5790.170'), OperatingSystem::MACOS()), + 'https://dynamic-url-3/', + ]; + + yield 'new version windows' => [ + new Driver(DriverName::CHROME(), Version::fromString('115.0.5790.170'), OperatingSystem::WINDOWS()), + 'https://dynamic-url-1/', + ]; + } + + /** + * @dataProvider byDriverDataProvider + */ + public function testByDriver(Driver $driver, string $expectedUrl): void + { + self::assertSame($expectedUrl, $this->urlResolver->byDriver($driver)); + } + + protected function setUp(): void + { + $httpClientMock = new MockHttpClient( + static function (string $method, string $url): MockResponse { + if ($method === 'GET') { + if ($url === 'https://googlechromelabs.github.io/chrome-for-testing/latest-patch-versions-per-build-with-downloads.json') { + return new MockResponse( + json_encode([ + 'builds' => [ + '115.0.5790' => [ + 'downloads' => [ + 'chromedriver' => [ + ['platform' => 'win32', 'url' => 'https://dynamic-url-1/'], + ['platform' => 'linux64', 'url' => 'https://dynamic-url-2/'], + ['platform' => 'mac-x64', 'url' => 'https://dynamic-url-3/'], + ], + ], + ], + ], + ]) + ); + } + } + + return new MockResponse( + 'NoSuchKeyThe specified key does not exist.
No such object: chromedriver/LATEST_RELEASE_xxx
', + ['http_code' => 404] + ); + } + ); + + $this->urlResolver = new DownloadUrlResolver($httpClientMock); + } +} diff --git a/tests/Driver/ChromeDriver/DownloaderTest.php b/tests/Driver/ChromeDriver/DownloaderTest.php index 6f38dac..c236f01 100644 --- a/tests/Driver/ChromeDriver/DownloaderTest.php +++ b/tests/Driver/ChromeDriver/DownloaderTest.php @@ -6,6 +6,7 @@ use DBrekelmans\BrowserDriverInstaller\Archive\Extractor; use DBrekelmans\BrowserDriverInstaller\Driver\ChromeDriver\Downloader; +use DBrekelmans\BrowserDriverInstaller\Driver\DownloadUrlResolver; use DBrekelmans\BrowserDriverInstaller\Driver\Driver; use DBrekelmans\BrowserDriverInstaller\Driver\DriverName; use DBrekelmans\BrowserDriverInstaller\OperatingSystem\OperatingSystem; @@ -30,6 +31,9 @@ class DownloaderTest extends TestCase /** @var Stub&Extractor */ private $archiveExtractor; + /** @var MockObject&DownloadUrlResolver */ + private $downloadUrlResolver; + /** @var MockObject&HttpClientInterface */ private $httpClient; @@ -49,13 +53,20 @@ public function testDownloadMac(): void { $this->mockFsAndArchiveExtractorForSuccessfulDownload(OperatingSystem::MACOS()); + $chromeDriverMac = new Driver(DriverName::CHROME(), Version::fromString('86.0.4240.22'), OperatingSystem::MACOS()); + + $this->downloadUrlResolver + ->expects(self::once()) + ->method('byDriver') + ->with($chromeDriverMac) + ->willReturn('https://chromedriver.storage.googleapis.com/86.0.4240.22/chromedriver_mac64.zip'); + $this->httpClient ->expects(self::atLeastOnce()) ->method('request') ->with('GET', 'https://chromedriver.storage.googleapis.com/86.0.4240.22/chromedriver_mac64.zip'); - $chromeDriverMac = new Driver(DriverName::CHROME(), Version::fromString('86.0.4240.22'), OperatingSystem::MACOS()); - $filePath = $this->downloader->download($chromeDriverMac, '.'); + $filePath = $this->downloader->download($chromeDriverMac, '.'); self::assertEquals('./chromedriver', $filePath); } @@ -64,28 +75,20 @@ public function testDownloadMacJson(): void { $this->mockFsAndArchiveExtractorForSuccessfulDownload(OperatingSystem::MACOS(), true); - $this->httpClient - ->expects(self::atLeastOnce()) - ->method('request') - ->with('GET', 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/115.0.5790.170/mac-x64/chromedriver-mac-x64.zip'); - $chromeDriverMac = new Driver(DriverName::CHROME(), Version::fromString('115.0.5790.170'), OperatingSystem::MACOS()); - $filePath = $this->downloader->download($chromeDriverMac, '.'); - self::assertEquals('./chromedriver', $filePath); - } - - public function testDownloadMacJsonNewEndpoint(): void - { - $this->mockFsAndArchiveExtractorForSuccessfulDownload(OperatingSystem::MACOS(), true); + $this->downloadUrlResolver + ->expects(self::once()) + ->method('byDriver') + ->with($chromeDriverMac) + ->willReturn('https://dynamic-download-url/driver.zip'); $this->httpClient ->expects(self::atLeastOnce()) ->method('request') - ->with('GET', 'https://storage.googleapis.com/chrome-for-testing-public/121.0.6167.0/mac-x64/chromedriver-mac-x64.zip'); + ->with('GET', 'https://dynamic-download-url/driver.zip'); - $chromeDriverMac = new Driver(DriverName::CHROME(), Version::fromString('121.0.6167.0'), OperatingSystem::MACOS()); - $filePath = $this->downloader->download($chromeDriverMac, '.'); + $filePath = $this->downloader->download($chromeDriverMac, '.'); self::assertEquals('./chromedriver', $filePath); } @@ -94,13 +97,20 @@ public function testDownloadLinux(): void { $this->mockFsAndArchiveExtractorForSuccessfulDownload(OperatingSystem::LINUX()); + $chromeDriverLinux = new Driver(DriverName::CHROME(), Version::fromString('86.0.4240.22'), OperatingSystem::LINUX()); + + $this->downloadUrlResolver + ->expects(self::once()) + ->method('byDriver') + ->with($chromeDriverLinux) + ->willReturn('https://chromedriver.storage.googleapis.com/86.0.4240.22/chromedriver_linux64.zip'); + $this->httpClient ->expects(self::atLeastOnce()) ->method('request') ->with('GET', 'https://chromedriver.storage.googleapis.com/86.0.4240.22/chromedriver_linux64.zip'); - $chromeDriverLinux = new Driver(DriverName::CHROME(), Version::fromString('86.0.4240.22'), OperatingSystem::LINUX()); - $filePath = $this->downloader->download($chromeDriverLinux, '.'); + $filePath = $this->downloader->download($chromeDriverLinux, '.'); self::assertEquals('./chromedriver', $filePath); } @@ -109,28 +119,20 @@ public function testDownloadLinuxJson(): void { $this->mockFsAndArchiveExtractorForSuccessfulDownload(OperatingSystem::LINUX(), true); - $this->httpClient - ->expects(self::atLeastOnce()) - ->method('request') - ->with('GET', 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/115.0.5790.170/linux64/chromedriver-linux64.zip'); - $chromeDriverLinux = new Driver(DriverName::CHROME(), Version::fromString('115.0.5790.170'), OperatingSystem::LINUX()); - $filePath = $this->downloader->download($chromeDriverLinux, '.'); - - self::assertEquals('./chromedriver', $filePath); - } - public function testDownloadLinuxJsonNewEndpoint(): void - { - $this->mockFsAndArchiveExtractorForSuccessfulDownload(OperatingSystem::LINUX(), true); + $this->downloadUrlResolver + ->expects(self::once()) + ->method('byDriver') + ->with($chromeDriverLinux) + ->willReturn('https://dynamic-download-url/driver.zip'); $this->httpClient ->expects(self::atLeastOnce()) ->method('request') - ->with('GET', 'https://storage.googleapis.com/chrome-for-testing-public/121.0.6167.0/linux64/chromedriver-linux64.zip'); + ->with('GET', 'https://dynamic-download-url/driver.zip'); - $chromeDriverLinux = new Driver(DriverName::CHROME(), Version::fromString('121.0.6167.0'), OperatingSystem::LINUX()); - $filePath = $this->downloader->download($chromeDriverLinux, '.'); + $filePath = $this->downloader->download($chromeDriverLinux, '.'); self::assertEquals('./chromedriver', $filePath); } @@ -139,13 +141,20 @@ public function testDownloadWindows(): void { $this->mockFsAndArchiveExtractorForSuccessfulDownload(OperatingSystem::WINDOWS()); + $chromeDriverWindows = new Driver(DriverName::CHROME(), Version::fromString('86.0.4240.22'), OperatingSystem::WINDOWS()); + + $this->downloadUrlResolver + ->expects(self::once()) + ->method('byDriver') + ->with($chromeDriverWindows) + ->willReturn('https://chromedriver.storage.googleapis.com/86.0.4240.22/chromedriver_win32.zip'); + $this->httpClient ->expects(self::atLeastOnce()) ->method('request') ->with('GET', 'https://chromedriver.storage.googleapis.com/86.0.4240.22/chromedriver_win32.zip'); - $chromeDriverLinux = new Driver(DriverName::CHROME(), Version::fromString('86.0.4240.22'), OperatingSystem::WINDOWS()); - $filePath = $this->downloader->download($chromeDriverLinux, '.'); + $filePath = $this->downloader->download($chromeDriverWindows, '.'); self::assertEquals('./chromedriver.exe', $filePath); } @@ -154,38 +163,31 @@ public function testDownloadWindowsJson(): void { $this->mockFsAndArchiveExtractorForSuccessfulDownload(OperatingSystem::WINDOWS(), true); - $this->httpClient - ->expects(self::atLeastOnce()) - ->method('request') - ->with('GET', 'https://edgedl.me.gvt1.com/edgedl/chrome/chrome-for-testing/115.0.5790.170/win32/chromedriver-win32.zip'); - $chromeDriverWindows = new Driver(DriverName::CHROME(), Version::fromString('115.0.5790.170'), OperatingSystem::WINDOWS()); - $filePath = $this->downloader->download($chromeDriverWindows, '.'); - self::assertEquals('./chromedriver.exe', $filePath); - } - - public function testDownloadWindowsJsonNewEndpoint(): void - { - $this->mockFsAndArchiveExtractorForSuccessfulDownload(OperatingSystem::WINDOWS(), true); + $this->downloadUrlResolver + ->expects(self::once()) + ->method('byDriver') + ->with($chromeDriverWindows) + ->willReturn('https://dynamic-download-url/driver.zip'); $this->httpClient ->expects(self::atLeastOnce()) ->method('request') - ->with('GET', 'https://storage.googleapis.com/chrome-for-testing-public/121.0.6167.0/win32/chromedriver-win32.zip'); + ->with('GET', 'https://dynamic-download-url/driver.zip'); - $chromeDriverWindows = new Driver(DriverName::CHROME(), Version::fromString('121.0.6167.0'), OperatingSystem::WINDOWS()); - $filePath = $this->downloader->download($chromeDriverWindows, '.'); + $filePath = $this->downloader->download($chromeDriverWindows, '.'); self::assertEquals('./chromedriver.exe', $filePath); } protected function setUp(): void { - $this->filesystem = $this->createStub(Filesystem::class); - $this->httpClient = $this->createMock(HttpClientInterface::class); - $this->archiveExtractor = $this->createStub(Extractor::class); - $this->downloader = new Downloader($this->filesystem, $this->httpClient, $this->archiveExtractor); + $this->filesystem = $this->createStub(Filesystem::class); + $this->httpClient = $this->createMock(HttpClientInterface::class); + $this->archiveExtractor = $this->createStub(Extractor::class); + $this->downloadUrlResolver = $this->createMock(DownloadUrlResolver::class); + $this->downloader = new Downloader($this->filesystem, $this->httpClient, $this->archiveExtractor, $this->downloadUrlResolver); } private function mockFsAndArchiveExtractorForSuccessfulDownload( diff --git a/tests/Driver/ChromeDriver/VersionResolverTest.php b/tests/Driver/ChromeDriver/VersionResolverTest.php index 6aa3d2e..0b94aac 100644 --- a/tests/Driver/ChromeDriver/VersionResolverTest.php +++ b/tests/Driver/ChromeDriver/VersionResolverTest.php @@ -81,6 +81,12 @@ public function testLatest(): void self::assertEquals(Version::fromString('86.0.4240.22'), $this->versionResolver->latest()); } + public function testIsJsonVersion(): void + { + self::assertFalse(VersionResolver::isJsonVersion(Version::fromString('114.0.5735.90'))); + self::assertTrue(VersionResolver::isJsonVersion(Version::fromString('115.0.5751'))); + } + protected function setUp(): void { $httpClientMock = new MockHttpClient(