diff --git a/core/modules/rest/tests/src/Functional/ResourceTestBase.php b/core/modules/rest/tests/src/Functional/ResourceTestBase.php index c7dfc69999..071ab9a6be 100644 --- a/core/modules/rest/tests/src/Functional/ResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/ResourceTestBase.php @@ -348,7 +348,7 @@ protected function request($method, Url $url, array $request_options) { $request_options[RequestOptions::HTTP_ERRORS] = FALSE; $request_options[RequestOptions::ALLOW_REDIRECTS] = FALSE; $request_options = $this->decorateWithXdebugCookie($request_options); - $client = $this->getSession()->getDriver()->getClient()->getClient(); + $client = $this->getHttpClient(); return $client->request($method, $url->setAbsolute(TRUE)->toString(), $request_options); } diff --git a/core/modules/system/tests/modules/test_page_test/src/Controller/Test.php b/core/modules/system/tests/modules/test_page_test/src/Controller/Test.php index e754eaf0c2..58797ce2fe 100644 --- a/core/modules/system/tests/modules/test_page_test/src/Controller/Test.php +++ b/core/modules/system/tests/modules/test_page_test/src/Controller/Test.php @@ -4,6 +4,7 @@ use Drupal\Core\Url; use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\HttpException; /** @@ -111,6 +112,17 @@ public function renderPipeInLink() { return ['#markup' => 'foo|bar|baz']; } + /** + * Renders all cookies from request. + * + * @return \Symfony\Component\HttpFoundation\Response + * The response object. + */ + public function renderRequestCookies() { + $cookies = \Drupal::request()->cookies->all(); + return new Response(json_encode($cookies), 200); + } + /** * Loads a page that does a redirect. * diff --git a/core/modules/system/tests/modules/test_page_test/test_page_test.routing.yml b/core/modules/system/tests/modules/test_page_test/test_page_test.routing.yml index e64a88d003..3dedee720a 100644 --- a/core/modules/system/tests/modules/test_page_test/test_page_test.routing.yml +++ b/core/modules/system/tests/modules/test_page_test/test_page_test.routing.yml @@ -90,3 +90,11 @@ test_page_test.meta_refresh: _controller: '\Drupal\test_page_test\Controller\Test::metaRefresh' requirements: _access: 'TRUE' + +test_page_test.request_cookies: + path: '/test-request-cookies' + defaults: + _title: 'Request Cookies' + _controller: '\Drupal\test_page_test\Controller\Test::renderRequestCookies' + requirements: + _access: 'TRUE' diff --git a/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php b/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php index 95ee39fd3a..9130e92d5f 100644 --- a/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php +++ b/core/tests/Drupal/FunctionalTests/BrowserTestBaseTest.php @@ -8,6 +8,9 @@ use Drupal\Core\Url; use Drupal\Tests\BrowserTestBase; use Drupal\Tests\Traits\Core\CronRunTrait; +use GuzzleHttp\Cookie\CookieJar; +use GuzzleHttp\Cookie\SetCookie; +use Symfony\Component\BrowserKit\Cookie; /** * Tests BrowserTestBase functionality. @@ -639,4 +642,77 @@ public function testGetDefaultDriveInstance() { $this->assertEquals([NULL, ['key1' => ['key2' => ['key3' => 3, 'key3.1' => 3.1]]]], $this->minkDefaultDriverArgs); } + /** + * Tests the ::decorateCookie() and ::decorateHeadersCookie methods. + * + * @dataProvider providerTestDecorationCookies + */ + public function testDecorationCookies($request_options, $is_force, $expected) { + $session_cookie = new Cookie('session', 'session-value'); + $client = $this->getSession()->getDriver()->getClient(); + $client->getCookieJar()->set($session_cookie); + + $request_options = $this->decorateRequestOptions($request_options, $is_force); + + $url = Url::fromRoute('test_page_test.request_cookies')->setAbsolute()->toString(); + $response = $this->getHttpClient()->request('GET', $url, $request_options); + $this->assertSame($expected, (string) $response->getBody()); + } + + /** + * Data provider for testDecorationCookies. + * + * @return array + * An array containing: + * - request options + * - force flag + * - expected response + */ + public function providerTestDecorationCookies() { + $domain = parse_url(getenv('SIMPLETEST_BASE_URL'), PHP_URL_HOST); + $request_cookie = new SetCookie([ + 'Name' => 'request', + 'Value' => 'request-value', + 'Domain' => $domain, + ]); + $request_cookie_override = new SetCookie([ + 'Name' => 'session', + 'Value' => 'request-value', + 'Domain' => $domain, + ]); + + return [ + 'Cookie combine values' => [ + ['cookies' => new CookieJar(TRUE, [$request_cookie])], + FALSE, + '{"request":"request-value","session":"session-value"}', + ], + 'Cookie non-force regim' => [ + ['cookies' => new CookieJar(TRUE, [$request_cookie_override])], + FALSE, + '{"session":"request-value"}', + ], + 'Cookie force regim' => [ + ['cookies' => new CookieJar(TRUE, [$request_cookie_override])], + TRUE, + '{"session":"session-value"}', + ], + 'Headers["Cookie"] combine values' => [ + ['headers' => ['Cookie' => 'request=request-value']], + FALSE, + '{"request":"request-value","session":"session-value"}', + ], + 'Headers["Cookie"] non-force regim' => [ + ['headers' => ['Cookie' => 'session=request-value']], + FALSE, + '{"session":"request-value"}', + ], + 'Headers["Cookie"] force regim' => [ + ['headers' => ['Cookie' => 'session=request-value']], + TRUE, + '{"session":"session-value"}', + ], + ]; + } + } diff --git a/core/tests/Drupal/FunctionalTests/GetHttpClientWithXdebugTest.php b/core/tests/Drupal/FunctionalTests/GetHttpClientWithXdebugTest.php new file mode 100644 index 0000000000..832ad34322 --- /dev/null +++ b/core/tests/Drupal/FunctionalTests/GetHttpClientWithXdebugTest.php @@ -0,0 +1,95 @@ +setAbsolute()->toString(); + if ($is_decorate) { + $request_options = $this->decorateWithXdebugCookieFromRequest($request_options, NULL); + } + $response = $this->getHttpClient()->request('GET', $url, $request_options); + $this->assertSame($expected, (string) $response->getBody()); + } + + /** + * Data provider for testGetHttpClientWithXdebugMode. + * + * @return array + * An array containing: + * - request options + * - decorate flag + * - expected response + */ + public function providerTestGetHttpClientXdebugMode() { + putenv('XDEBUG_CONFIG=idekey=cake'); + $headers_options = [RequestOptions::HEADERS => ['Cookie' => 'custom=cooking']]; + $cookie_jar = new CookieJar(TRUE, [ + [ + 'Name' => 'custom', + 'Value' => 'cooking', + 'Domain' => parse_url(getenv('SIMPLETEST_BASE_URL'), PHP_URL_HOST), + ], + ]); + $cookies_options = [RequestOptions::COOKIES => $cookie_jar]; + + return [ + 'Headers empty' => [ + [], + TRUE, + '{"XDEBUG_SESSION":"cake"}', + ], + 'Headers with decorate' => [ + $headers_options, + TRUE, + '{"custom":"cooking","XDEBUG_SESSION":"cake"}', + ], + 'Headers without decorate' => [ + $headers_options, + FALSE, + '{"custom":"cooking"}', + ], + 'Cookies empty' => [ + [], + TRUE, + '{"XDEBUG_SESSION":"cake"}', + ], + 'Cookies with decorate' => [ + $cookies_options, + TRUE, + '{"custom":"cooking","XDEBUG_SESSION":"cake"}', + ], + 'Cookies without decorate' => [ + $cookies_options, + FALSE, + '{"custom":"cooking"}', + ], + ]; + } + +} diff --git a/core/tests/Drupal/Tests/BrowserTestBase.php b/core/tests/Drupal/Tests/BrowserTestBase.php index b12ba60b8a..074b728466 100644 --- a/core/tests/Drupal/Tests/BrowserTestBase.php +++ b/core/tests/Drupal/Tests/BrowserTestBase.php @@ -22,6 +22,7 @@ use Drupal\Tests\block\Traits\BlockCreationTrait; use Drupal\Tests\node\Traits\ContentTypeCreationTrait; use Drupal\Tests\node\Traits\NodeCreationTrait; +use Drupal\Tests\Traits\DecorateCookiesTrait; use Drupal\Tests\user\Traits\UserCreationTrait; use PHPUnit\Framework\TestCase; use Psr\Http\Message\RequestInterface; @@ -66,6 +67,7 @@ createUser as drupalCreateUser; } use XdebugRequestTrait; + use DecorateCookiesTrait; /** * The database prefix of this test run. @@ -296,12 +298,13 @@ protected function initMink() { if ($driver instanceof GoutteDriver) { // Turn off curl timeout. Having a timeout is not a problem in a normal // test running, but it is a problem when debugging. Also, disable SSL - // peer verification so that testing under HTTPS always works. + // peer verification so that testing under HTTPS always works. Include + // cookies with XDEBUG_SESSION in the debugging. /** @var \GuzzleHttp\Client $client */ $client = $this->container->get('http_client_factory')->fromOptions([ 'timeout' => NULL, 'verify' => FALSE, - ]); + ] + $this->decorateWithXdebugCookieFromRequest()); // Inject a Guzzle middleware to generate debug output for every request // performed in the test. @@ -577,6 +580,33 @@ public function getSession($name = NULL) { return $this->mink->getSession($name); } + /** + * Obtain the HTTP client for the system under test. + * + * Use this method for arbitrary HTTP requests to the site under test. For + * most tests, you should not get the HTTP client and instead use navigation + * methods such as drupalGet() and clickLink() in order to benefit from + * assertions. + * + * Subclasses which substitute a different Mink driver should override this + * method and provide a Guzzle client if the Mink driver provides one. + * + * @return \GuzzleHttp\ClientInterface + * The client with BrowserTestBase configuration. + * + * @throws \RuntimeException + * If the Mink driver does not support a Guzzle HTTP client, throw an + * exception. + */ + protected function getHttpClient() { + /* @var $mink_driver \Behat\Mink\Driver\DriverInterface */ + $mink_driver = $this->getSession()->getDriver(); + if ($mink_driver instanceof GoutteDriver) { + return $mink_driver->getClient()->getClient(); + } + throw new \RuntimeException ('The Mink client type ' . get_class($mink_driver) . ' does not support getHttpClient().'); + } + /** * Returns WebAssert object. * @@ -656,11 +686,13 @@ protected function buildUrl($path, array $options = []) { * to set for example the "Accept-Language" header for requesting the page * in a different language. Note that not all headers are supported, for * example the "Accept" header is always overridden by the browser. For - * testing REST APIs it is recommended to directly use an HTTP client such - * as Guzzle instead. + * testing REST APIs it is recommended to obtain a separate HTTP client + * using getHttpClient() and performing requests that way. * * @return string * The retrieved HTML string, also available as $this->getRawContent() + * + * @see \Drupal\Tests\BrowserTestBase::getHttpClient() */ protected function drupalGet($path, array $options = [], array $headers = []) { $options['absolute'] = TRUE; diff --git a/core/tests/Drupal/Tests/Core/Test/BrowserTestBaseTest.php b/core/tests/Drupal/Tests/Core/Test/BrowserTestBaseTest.php new file mode 100644 index 0000000000..a8eb65f667 --- /dev/null +++ b/core/tests/Drupal/Tests/Core/Test/BrowserTestBaseTest.php @@ -0,0 +1,82 @@ +getMockBuilder(Session::class) + ->disableOriginalConstructor() + ->setMethods(['getDriver']) + ->getMock(); + $session->expects($this->once()) + ->method('getDriver') + ->willReturn($driver); + + $btb = $this->getMockBuilder(BrowserTestBase::class) + ->disableOriginalConstructor() + ->setMethods(['getSession']) + ->getMockForAbstractClass(); + $btb->expects($this->once()) + ->method('getSession') + ->willReturn($session); + + return $btb; + } + + /** + * @covers ::getHttpClient + */ + public function testGetHttpClient() { + // Our stand-in for the Guzzle client object. + $expected = new \stdClass(); + + $browserkit_client = $this->getMockBuilder(Client::class) + ->setMethods(['getClient']) + ->getMockForAbstractClass(); + $browserkit_client->expects($this->once()) + ->method('getClient') + ->willReturn($expected); + + // Because the driver is a GoutteDriver, we'll get back a client. + $driver = $this->getMockBuilder(GoutteDriver::class) + ->setMethods(['getClient']) + ->getMock(); + $driver->expects($this->once()) + ->method('getClient') + ->willReturn($browserkit_client); + + $btb = $this->mockBrowserTestBaseWithDriver($driver); + + $ref_gethttpclient = new \ReflectionMethod($btb, 'getHttpClient'); + $ref_gethttpclient->setAccessible(TRUE); + + $this->assertSame(get_class($expected), get_class($ref_gethttpclient->invoke($btb))); + } + + /** + * @covers ::getHttpClient + */ + public function testGetHttpClientException() { + // A driver type that isn't GoutteDriver. This should cause a + // RuntimeException. + $btb = $this->mockBrowserTestBaseWithDriver(new \stdClass()); + + $ref_gethttpclient = new \ReflectionMethod($btb, 'getHttpClient'); + $ref_gethttpclient->setAccessible(TRUE); + + $this->setExpectedException(\RuntimeException::class, 'The Mink client type stdClass does not support getHttpClient().'); + $ref_gethttpclient->invoke($btb); + } + +} diff --git a/core/tests/Drupal/Tests/Traits/DecorateCookiesTrait.php b/core/tests/Drupal/Tests/Traits/DecorateCookiesTrait.php new file mode 100644 index 0000000000..e5a29bc559 --- /dev/null +++ b/core/tests/Drupal/Tests/Traits/DecorateCookiesTrait.php @@ -0,0 +1,156 @@ +decorateCookie($request_options, $force_regim); + } + if (isset($request_options['headers']['cookie'])) { + $request_options = $this->decorateHeadersCookie($request_options, $force_regim); + } + + if ( + !isset($request_options['cookies']) && + !isset($request_options['headers']['cookie']) + ) { + $request_options = $this->decorateHeadersCookie($request_options, $force_regim); + } + + return $request_options; + } + + /** + * Adds cookies to the headers cookie of request options. + * + * @param array $request_options + * (optional) The request options. + * @param bool $force_regim + * (optional) Override exists cookies. + * + * @return array + * Request options updated with all cookies from session. + */ + protected function decorateHeadersCookie(array $request_options = [], $force_regim = FALSE) { + $session = $this->getSession(); + $driver = $session->getDriver(); + + if ($driver instanceof BrowserKitDriver) { + $client = $driver->getClient(); + $decorate_cookies = $client->getCookieJar()->all(); + } + else { + $decorate_cookies = []; + } + + if (isset($request_options[RequestOptions::HEADERS]['Cookie'])) { + if (function_exists('http_parse_cookie')) { + $request_cookies = http_parse_cookie($request_options[RequestOptions::HEADERS]['Cookie']); + } + else { + $cookies = explode(';', $request_options[RequestOptions::HEADERS]['Cookie']); + foreach ($cookies as $cookie) { + list($key, $value) = explode('=', $cookie, 2); + $request_cookies[trim($key)] = trim($value); + } + } + } + else { + $request_cookies = []; + } + + foreach ($decorate_cookies as $cookie) { + if (!isset($request_cookies[$cookie->getName()]) || $force_regim) { + $request_cookies[$cookie->getName()] = $cookie->getValue(); + } + } + + if (!empty($request_cookies)) { + if (function_exists('http_build_cookie')) { + $request_options[RequestOptions::HEADERS]['Cookie'] = http_build_cookie($request_cookies); + } + else { + $cookies = []; + foreach ($request_cookies as $name => $value) { + $cookies[] = "$name=$value"; + } + $request_options[RequestOptions::HEADERS]['Cookie'] = implode('; ', $cookies); + } + } + + return $request_options; + } + + /** + * Adds cookies to the headers cookie of request options. + * + * @param array $request_options + * (optional) The request options. + * @param bool $force_regim + * (optional) Override exists cookies. + * + * @return array + * Request options updated with all cookies from session. + */ + protected function decorateCookie(array $request_options = [], $force_regim = FALSE) { + $session = $this->getSession(); + $driver = $session->getDriver(); + + if ($driver instanceof BrowserKitDriver) { + $client = $driver->getClient(); + $decorate_cookies = $client->getCookieJar()->all(); + } + else { + $decorate_cookies = []; + } + + if (isset($request_options[RequestOptions::COOKIES])) { + $request_cookies = $request_options[RequestOptions::COOKIES]; + } + else { + $request_cookies = new CookieJar(); + } + + $cookies = []; + foreach ($decorate_cookies as $cookie) { + if (!$request_cookies->getCookieByName($cookie->getName()) || $force_regim) { + $new_cookie = new SetCookie([ + 'Name' => $cookie->getName(), + 'Value' => $cookie->getRawValue(), + 'Domain' => $cookie->getDomain() ?: \Drupal::request()->getHost(), + 'Path' => $cookie->getPath(), + 'Expires' => $cookie->getExpiresTime(), + ]); + $cookies[] = $new_cookie; + $request_cookies->setCookie($new_cookie); + } + } + + if (!empty($request_cookies->toArray())) { + $request_options[RequestOptions::COOKIES] = $request_cookies; + } + + return $request_options; + } +} \ No newline at end of file diff --git a/core/tests/Drupal/Tests/XdebugRequestTrait.php b/core/tests/Drupal/Tests/XdebugRequestTrait.php index 5da86a51bd..9ddd3ab274 100644 --- a/core/tests/Drupal/Tests/XdebugRequestTrait.php +++ b/core/tests/Drupal/Tests/XdebugRequestTrait.php @@ -2,6 +2,9 @@ namespace Drupal\Tests; +use GuzzleHttp\Cookie\CookieJar; +use GuzzleHttp\Cookie\SetCookie; +use GuzzleHttp\RequestOptions; use Symfony\Component\HttpFoundation\Request; trait XdebugRequestTrait { @@ -45,4 +48,55 @@ protected function extractCookiesFromRequest(Request $request) { return $cookies; } + /** + * Adds the Xdebug cookie to the request options. + * + * @param array $request_options + * (optional) The request options. + * @param \Symfony\Component\HttpFoundation\Request|null $request + * (optional) The request. + * + * @return array + * Request options updated with the Xdebug cookie if present. + */ + protected function decorateWithXdebugCookieFromRequest(array $request_options = [], $request = NULL) { + if (!$request) { + $request = \Drupal::request(); + } + $xdebug_cookies = $this->extractCookiesFromRequest($request); + if ($xdebug_cookies) { + + if ( + isset($request_options[RequestOptions::HEADERS]['Cookie']) || + !isset($request_options[RequestOptions::COOKIES]) + ) { + $cookies = 'XDEBUG_SESSION=' . current($xdebug_cookies['XDEBUG_SESSION']); + if (isset($request_options[RequestOptions::HEADERS]['Cookie'])) { + $request_options[RequestOptions::HEADERS]['Cookie'] .= '; ' . $cookies; + } + else { + $request_options[RequestOptions::HEADERS]['Cookie'] = $cookies; + } + } + + if (isset($request_options[RequestOptions::COOKIES])) { + $cookies = []; + $new_cookie = new SetCookie([ + 'Name' => 'XDEBUG_SESSION', + 'Value' => current($xdebug_cookies['XDEBUG_SESSION']), + 'Domain' => $request->getHost(), + ]); + if (isset($request_options[RequestOptions::COOKIES])) { + foreach ($request_options[RequestOptions::COOKIES] as $cookie) { + $cookies[] = $cookie; + } + } + $cookies[] = $new_cookie; + $request_options[RequestOptions::COOKIES] = new CookieJar(TRUE, $cookies); + } + } + + return $request_options; + } + }