diff -u b/core/modules/update/tests/src/Unit/UpdateFetcherTest.php b/core/modules/update/tests/src/Unit/UpdateFetcherTest.php --- b/core/modules/update/tests/src/Unit/UpdateFetcherTest.php +++ b/core/modules/update/tests/src/Unit/UpdateFetcherTest.php @@ -5,10 +5,17 @@ use Drupal\Core\Site\Settings; use Drupal\Tests\UnitTestCase; use Drupal\update\UpdateFetcher; +use GuzzleHttp\Client; +use GuzzleHttp\Handler\MockHandler; +use GuzzleHttp\HandlerStack; +use GuzzleHttp\Middleware; +use GuzzleHttp\Psr7\Response; /** * Tests update functionality unrelated to the database. * + * @coversDefaultClass \Drupal\update\UpdateFetcher + * * @group update */ class UpdateFetcherTest extends UnitTestCase { @@ -21,13 +28,51 @@ protected $updateFetcher; /** + * History of requests/responses. + * + * @var array + */ + protected $history = []; + + /** + * Mock HTTP client. + * + * @var \GuzzleHttp\ClientInterface + */ + protected $mockHttpClient; + + /** + * Mock config factory. + * + * @var \Drupal\Core\Config\ConfigFactoryInterface + */ + protected $mockConfigFactory; + + /** + * A test project to fetch with. + * + * @var array + */ + protected $testProject; + + /** * {@inheritdoc} */ protected function setUp(): void { - $config_factory = $this->getConfigFactoryStub(['update.settings' => ['fetch_url' => 'http://www.example.com']]); - $http_client_mock = $this->createMock('\GuzzleHttp\ClientInterface'); + parent::setUp(); + $this->mockConfigFactory = $this->getConfigFactoryStub(['update.settings' => ['fetch_url' => 'http://www.example.com']]); + $this->mockHttpClient = $this->createMock('\GuzzleHttp\ClientInterface'); $settings = new Settings([]); - $this->updateFetcher = new UpdateFetcher($config_factory, $http_client_mock, $settings); + $this->updateFetcher = new UpdateFetcher($this->mockConfigFactory, $this->mockHttpClient, $settings); + $this->testProject = [ + 'name' => 'update_test', + 'project_type' => '', + 'info' => [ + 'version' => '', + 'project status url' => 'https://www.example.com', + ], + 'includes' => ['module1' => 'Module 1', 'module2' => 'Module 2'], + ]; } /** @@ -101,2 +146,92 @@ + /** + * 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) { + // 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() { + // 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); + + $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); + } + + /** + * @covers ::doRequest + * @covers ::fetchProjectData + */ + public function testUpdateFetcherHttpFallback() { + $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); + + $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); + } + +} + +namespace Drupal\update; +use Drupal\Core\Logger\RfcLogLevel; +/** + * Mock version of watchdog_exception(). + * + * This is unfortunate, but UpdateFetcher::doRequest() calls + * watchdog_exception(), which is defined in includes/bootstrap.inc, which + * cannot be included for Unit tests or we break unit test encapsulation. + * Therefore, we define our own copy in the \Drupal\update namespace that + * doesn't do anything (we're not trying to test the exception logging). + * + * @todo Remove in https://www.drupal.org/project/drupal/issues/2932518 + */ +function watchdog_exception($type, \Exception $exception, $message = NULL, $variables = [], $severity = RfcLogLevel::ERROR, $link = NULL) { + // No-op. }