mockConfigFactory = $this->getConfigFactoryStub(['update.settings' => ['fetch_url' => 'http://www.example.com']]); $this->mockHttpClient = $this->createMock('\GuzzleHttp\ClientInterface'); $settings = new Settings([]); $this->logger = new TestLogger(); $this->updateFetcher = new UpdateFetcher($this->mockConfigFactory, $this->mockHttpClient, $settings, $this->logger); $this->testProject = [ 'name' => 'update_test', 'project_type' => '', 'info' => [ 'version' => '', 'project status url' => 'https://www.example.com', ], 'includes' => ['module1' => 'Module 1', 'module2' => 'Module 2'], ]; } /** * Tests that buildFetchUrl() builds the URL correctly. * * @param array $project * A keyed array of project information matching results from * \Drupal\update\UpdateManager::getProjects(). * @param string $site_key * A string to mimic an anonymous site key hash. * @param string $expected * The expected URL returned from UpdateFetcher::buildFetchUrl() * * @dataProvider providerTestUpdateBuildFetchUrl * * @see \Drupal\update\UpdateFetcher::buildFetchUrl() */ public function testUpdateBuildFetchUrl(array $project, $site_key, $expected): void { $url = $this->updateFetcher->buildFetchUrl($project, $site_key); $this->assertEquals($url, $expected); $this->assertFalse($this->logger->hasErrorRecords()); } /** * Provide test data for self::testUpdateBuildFetchUrl(). * * @return array * An array of arrays, each containing: * - 'project' - An array matching a project's .info file structure. * - 'site_key' - An arbitrary site key. * - 'expected' - The expected URL from UpdateFetcher::buildFetchUrl(). */ public static function providerTestUpdateBuildFetchUrl() { $data = []; // First test that we didn't break the trivial case. $project['name'] = 'update_test'; $project['project_type'] = ''; $project['info']['version'] = ''; $project['info']['project status url'] = 'http://www.example.com'; $project['includes'] = ['module1' => 'Module 1', 'module2' => 'Module 2']; $site_key = ''; $expected = "http://www.example.com/{$project['name']}/current"; $data[] = [$project, $site_key, $expected]; // For uninstalled projects it shouldn't add the site key either. $site_key = 'site_key'; $project['project_type'] = 'disabled'; $expected = "http://www.example.com/{$project['name']}/current"; $data[] = [$project, $site_key, $expected]; // For installed projects, test adding the site key. $project['project_type'] = ''; $expected = "http://www.example.com/{$project['name']}/current"; $expected .= '?site_key=site_key'; $expected .= '&list=' . rawurlencode('module1,module2'); $data[] = [$project, $site_key, $expected]; // Test when the URL contains a question mark. $project['info']['project status url'] = 'http://www.example.com/?project='; $expected = "http://www.example.com/?project=/{$project['name']}/current"; $expected .= '&site_key=site_key'; $expected .= '&list=' . rawurlencode('module1,module2'); $data[] = [$project, $site_key, $expected]; return $data; } /** * Mocks the HTTP client. * * @param \GuzzleHttp\Psr7\Response ...$responses * Variable number of Response objects that the mocked client should return. */ protected function mockClient(Response ...$responses): void { // Create a mock and queue responses. $mock_handler = new MockHandler($responses); $handler_stack = HandlerStack::create($mock_handler); $history = Middleware::history($this->history); $handler_stack->push($history); $this->mockHttpClient = new Client(['handler' => $handler_stack]); } /** * @covers ::doRequest * @covers ::fetchProjectData */ public function testUpdateFetcherNoFallback(): void { // First, try without the HTTP fallback setting, and HTTPS mocked to fail. $settings = new Settings([]); $this->mockClient( new Response(500, [], 'HTTPS failed'), ); $update_fetcher = new UpdateFetcher($this->mockConfigFactory, $this->mockHttpClient, $settings, $this->logger); $data = $update_fetcher->fetchProjectData($this->testProject, ''); // There should only be one request / response pair. $this->assertCount(1, $this->history); $request = $this->history[0]['request']; $this->assertNotEmpty($request); // It should have only been an HTTPS request. $this->assertEquals('https', $request->getUri()->getScheme()); // And it should have failed. $response = $this->history[0]['response']; $this->assertEquals(500, $response->getStatusCode()); $this->assertEmpty($data); $this->assertTrue($this->logger->hasErrorThatPasses(function (array $record) { return $record['context']['@message'] === "Server error: `GET https://www.example.com/update_test/current` resulted in a `500 Internal Server Error` response:\nHTTPS failed\n"; })); } /** * @covers ::doRequest * @covers ::fetchProjectData */ public function testUpdateFetcherHttpFallback(): void { $settings = new Settings(['update_fetch_with_http_fallback' => TRUE]); $this->mockClient( new Response(500, [], 'HTTPS failed'), new Response(200, [], 'HTTP worked'), ); $update_fetcher = new UpdateFetcher($this->mockConfigFactory, $this->mockHttpClient, $settings, $this->logger); $data = $update_fetcher->fetchProjectData($this->testProject, ''); // There should be two request / response pairs. $this->assertCount(2, $this->history); // The first should have been HTTPS and should have failed. $first_try = $this->history[0]; $this->assertNotEmpty($first_try); $this->assertEquals('https', $first_try['request']->getUri()->getScheme()); $this->assertEquals(500, $first_try['response']->getStatusCode()); // The second should have been the HTTP fallback and should have worked. $second_try = $this->history[1]; $this->assertNotEmpty($second_try); $this->assertEquals('http', $second_try['request']->getUri()->getScheme()); $this->assertEquals(200, $second_try['response']->getStatusCode()); // Although this is a bogus mocked response, it's what fetchProjectData() // should return in this case. $this->assertEquals('HTTP worked', $data); $this->assertTrue($this->logger->hasErrorThatPasses(function (array $record) { return $record['context']['@message'] === "Server error: `GET https://www.example.com/update_test/current` resulted in a `500 Internal Server Error` response:\nHTTPS failed\n"; })); } }