You are here

class ResourceResponseSubscriberTest in Drupal 9

Same name and namespace in other branches
  1. 8 core/modules/rest/tests/src/Unit/EventSubscriber/ResourceResponseSubscriberTest.php \Drupal\Tests\rest\Unit\EventSubscriber\ResourceResponseSubscriberTest

@coversDefaultClass \Drupal\rest\EventSubscriber\ResourceResponseSubscriber @group rest

Hierarchy

Expanded class hierarchy of ResourceResponseSubscriberTest

File

core/modules/rest/tests/src/Unit/EventSubscriber/ResourceResponseSubscriberTest.php, line 30

Namespace

Drupal\Tests\rest\Unit\EventSubscriber
View source
class ResourceResponseSubscriberTest extends UnitTestCase {

  /**
   * @covers ::onResponse
   * @dataProvider providerTestSerialization
   */
  public function testSerialization($data, $expected_response = FALSE) {
    $request = new Request();
    $route_match = new RouteMatch('test', new Route('/rest/test', [
      '_rest_resource_config' => 'restplugin',
    ], [
      '_format' => 'json',
    ]));
    $handler_response = new ResourceResponse($data);
    $resource_response_subscriber = $this
      ->getFunctioningResourceResponseSubscriber($route_match);
    $event = new ResponseEvent($this
      ->prophesize(HttpKernelInterface::class)
      ->reveal(), $request, HttpKernelInterface::MASTER_REQUEST, $handler_response);
    $resource_response_subscriber
      ->onResponse($event);

    // Content is a serialized version of the data we provided.
    $this
      ->assertEquals($expected_response !== FALSE ? $expected_response : Json::encode($data), $event
      ->getResponse()
      ->getContent());
  }
  public function providerTestSerialization() {
    return [
      // The default data for \Drupal\rest\ResourceResponse.
      'default' => [
        NULL,
        '',
      ],
      'empty string' => [
        '',
      ],
      'simple string' => [
        'string',
      ],
      // cSpell:disable-next-line
      'complex string' => [
        'Complex \\ string $%^&@ with unicode ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΣὨ',
      ],
      'empty array' => [
        [],
      ],
      'numeric array' => [
        [
          'test',
        ],
      ],
      'associative array' => [
        [
          'test' => 'foobar',
        ],
      ],
      'boolean true' => [
        TRUE,
      ],
      'boolean false' => [
        FALSE,
      ],
    ];
  }

  /**
   * @covers ::getResponseFormat
   *
   * Note this does *not* need to test formats being requested that are not
   * accepted by the server, because the routing system would have already
   * prevented those from reaching the controller.
   *
   * @dataProvider providerTestResponseFormat
   */
  public function testResponseFormat($methods, array $supported_response_formats, array $supported_request_formats, $request_format, array $request_headers, $request_body, $expected_response_format, $expected_response_content_type, $expected_response_content) {
    foreach ($request_headers as $key => $value) {
      unset($request_headers[$key]);
      $key = strtoupper(str_replace('-', '_', $key));
      $request_headers[$key] = $value;
    }
    foreach ($methods as $method) {
      $request = Request::create('/rest/test', $method, [], [], [], $request_headers, $request_body);

      // \Drupal\Core\StackMiddleware\NegotiationMiddleware normally takes care
      // of this so we'll hard code it here.
      if ($request_format) {
        $request
          ->setRequestFormat($request_format);
      }
      $route_requirements = $this
        ->generateRouteRequirements($supported_response_formats, $supported_request_formats);
      $route_match = new RouteMatch('test', new Route('/rest/test', [
        '_rest_resource_config' => $this
          ->randomMachineName(),
      ], $route_requirements));
      $resource_response_subscriber = new ResourceResponseSubscriber($this
        ->prophesize(SerializerInterface::class)
        ->reveal(), $this
        ->prophesize(RendererInterface::class)
        ->reveal(), $route_match);
      $this
        ->assertSame($expected_response_format, $resource_response_subscriber
        ->getResponseFormat($route_match, $request));
    }
  }

  /**
   * @covers ::onResponse
   * @covers ::getResponseFormat
   * @covers ::renderResponseBody
   * @covers ::flattenResponse
   *
   * @dataProvider providerTestResponseFormat
   */
  public function testOnResponseWithCacheableResponse($methods, array $supported_response_formats, array $supported_request_formats, $request_format, array $request_headers, $request_body, $expected_response_format, $expected_response_content_type, $expected_response_content) {
    foreach ($request_headers as $key => $value) {
      unset($request_headers[$key]);
      $key = strtoupper(str_replace('-', '_', $key));
      $request_headers[$key] = $value;
    }
    foreach ($methods as $method) {
      $request = Request::create('/rest/test', $method, [], [], [], $request_headers, $request_body);

      // \Drupal\Core\StackMiddleware\NegotiationMiddleware normally takes care
      // of this so we'll hard code it here.
      if ($request_format) {
        $request
          ->setRequestFormat($request_format);
      }
      $route_requirements = $this
        ->generateRouteRequirements($supported_response_formats, $supported_request_formats);
      $route_match = new RouteMatch('test', new Route('/rest/test', [
        '_rest_resource_config' => $this
          ->randomMachineName(),
      ], $route_requirements));

      // The RequestHandler must return a ResourceResponseInterface object.
      $handler_response = new ResourceResponse($method !== 'DELETE' ? [
        'REST' => 'Drupal',
      ] : NULL);
      $this
        ->assertInstanceOf(ResourceResponseInterface::class, $handler_response);
      $this
        ->assertInstanceOf(CacheableResponseInterface::class, $handler_response);

      // The ResourceResponseSubscriber must then generate a response body and
      // transform it to a plain CacheableResponse object.
      $resource_response_subscriber = $this
        ->getFunctioningResourceResponseSubscriber($route_match);
      $event = new ResponseEvent($this
        ->prophesize(HttpKernelInterface::class)
        ->reveal(), $request, HttpKernelInterface::MASTER_REQUEST, $handler_response);
      $resource_response_subscriber
        ->onResponse($event);
      $final_response = $event
        ->getResponse();
      $this
        ->assertNotInstanceOf(ResourceResponseInterface::class, $final_response);
      $this
        ->assertInstanceOf(CacheableResponseInterface::class, $final_response);
      $this
        ->assertSame($expected_response_content_type, $final_response->headers
        ->get('Content-Type'));
      $this
        ->assertEquals($expected_response_content, $final_response
        ->getContent());
    }
  }

  /**
   * @covers ::onResponse
   * @covers ::getResponseFormat
   * @covers ::renderResponseBody
   * @covers ::flattenResponse
   *
   * @dataProvider providerTestResponseFormat
   */
  public function testOnResponseWithUncacheableResponse($methods, array $supported_response_formats, array $supported_request_formats, $request_format, array $request_headers, $request_body, $expected_response_format, $expected_response_content_type, $expected_response_content) {
    foreach ($request_headers as $key => $value) {
      unset($request_headers[$key]);
      $key = strtoupper(str_replace('-', '_', $key));
      $request_headers[$key] = $value;
    }
    foreach ($methods as $method) {
      $request = Request::create('/rest/test', $method, [], [], [], $request_headers, $request_body);

      // \Drupal\Core\StackMiddleware\NegotiationMiddleware normally takes care
      // of this so we'll hard code it here.
      if ($request_format) {
        $request
          ->setRequestFormat($request_format);
      }
      $route_requirements = $this
        ->generateRouteRequirements($supported_response_formats, $supported_request_formats);
      $route_match = new RouteMatch('test', new Route('/rest/test', [
        '_rest_resource_config' => $this
          ->randomMachineName(),
      ], $route_requirements));

      // The RequestHandler must return a ResourceResponseInterface object.
      $handler_response = new ModifiedResourceResponse($method !== 'DELETE' ? [
        'REST' => 'Drupal',
      ] : NULL);
      $this
        ->assertInstanceOf(ResourceResponseInterface::class, $handler_response);
      $this
        ->assertNotInstanceOf(CacheableResponseInterface::class, $handler_response);

      // The ResourceResponseSubscriber must then generate a response body and
      // transform it to a plain Response object.
      $resource_response_subscriber = $this
        ->getFunctioningResourceResponseSubscriber($route_match);
      $event = new ResponseEvent($this
        ->prophesize(HttpKernelInterface::class)
        ->reveal(), $request, HttpKernelInterface::MASTER_REQUEST, $handler_response);
      $resource_response_subscriber
        ->onResponse($event);
      $final_response = $event
        ->getResponse();
      $this
        ->assertNotInstanceOf(ResourceResponseInterface::class, $final_response);
      $this
        ->assertNotInstanceOf(CacheableResponseInterface::class, $final_response);
      $this
        ->assertSame($expected_response_content_type, $final_response->headers
        ->get('Content-Type'));
      $this
        ->assertEquals($expected_response_content, $final_response
        ->getContent());
    }
  }

  /**
   * @return array
   *   0. methods to test
   *   1. supported formats for route requirements
   *   2. request format
   *   3. request headers
   *   4. request body
   *   5. expected response format
   *   6. expected response content type
   *   7. expected response body
   */
  public function providerTestResponseFormat() {
    $json_encoded = Json::encode([
      'REST' => 'Drupal',
    ]);
    $xml_encoded = "<?xml version=\"1.0\"?>\n<response><REST>Drupal</REST></response>\n";
    $safe_method_test_cases = [
      'safe methods: client requested format (JSON)' => [
        // @todo add 'HEAD' in https://www.drupal.org/node/2752325
        [
          'GET',
        ],
        [
          'xml',
          'json',
        ],
        [],
        'json',
        [],
        NULL,
        'json',
        'application/json',
        $json_encoded,
      ],
      'safe methods: client requested format (XML)' => [
        // @todo add 'HEAD' in https://www.drupal.org/node/2752325
        [
          'GET',
        ],
        [
          'xml',
          'json',
        ],
        [],
        'xml',
        [],
        NULL,
        'xml',
        'text/xml',
        $xml_encoded,
      ],
      'safe methods: client requested no format: response should use the first configured format (JSON)' => [
        // @todo add 'HEAD' in https://www.drupal.org/node/2752325
        [
          'GET',
        ],
        [
          'json',
          'xml',
        ],
        [],
        FALSE,
        [],
        NULL,
        'json',
        'application/json',
        $json_encoded,
      ],
      'safe methods: client requested no format: response should use the first configured format (XML)' => [
        // @todo add 'HEAD' in https://www.drupal.org/node/2752325
        [
          'GET',
        ],
        [
          'xml',
          'json',
        ],
        [],
        FALSE,
        [],
        NULL,
        'xml',
        'text/xml',
        $xml_encoded,
      ],
    ];
    $unsafe_method_bodied_test_cases = [
      'unsafe methods with response (POST, PATCH): client requested no format, response should use request body format (JSON)' => [
        [
          'POST',
          'PATCH',
        ],
        [
          'xml',
          'json',
        ],
        [
          'xml',
          'json',
        ],
        FALSE,
        [
          'Content-Type' => 'application/json',
        ],
        $json_encoded,
        'json',
        'application/json',
        $json_encoded,
      ],
      'unsafe methods with response (POST, PATCH): client requested no format, response should use request body format (XML)' => [
        [
          'POST',
          'PATCH',
        ],
        [
          'xml',
          'json',
        ],
        [
          'xml',
          'json',
        ],
        FALSE,
        [
          'Content-Type' => 'text/xml',
        ],
        $xml_encoded,
        'xml',
        'text/xml',
        $xml_encoded,
      ],
      'unsafe methods with response (POST, PATCH): client requested format other than request body format (JSON): response format should use requested format (XML)' => [
        [
          'POST',
          'PATCH',
        ],
        [
          'xml',
          'json',
        ],
        [
          'xml',
          'json',
        ],
        'xml',
        [
          'Content-Type' => 'application/json',
        ],
        $json_encoded,
        'xml',
        'text/xml',
        $xml_encoded,
      ],
      'unsafe methods with response (POST, PATCH): client requested format other than request body format (XML), but is allowed for the request body (JSON)' => [
        [
          'POST',
          'PATCH',
        ],
        [
          'xml',
          'json',
        ],
        [
          'xml',
          'json',
        ],
        'json',
        [
          'Content-Type' => 'text/xml',
        ],
        $xml_encoded,
        'json',
        'application/json',
        $json_encoded,
      ],
      'unsafe methods with response (POST, PATCH): client requested format other than request body format when only XML is allowed as a content type format' => [
        [
          'POST',
          'PATCH',
        ],
        [
          'xml',
        ],
        [
          'json',
        ],
        'json',
        [
          'Content-Type' => 'text/xml',
        ],
        $xml_encoded,
        'json',
        'application/json',
        $json_encoded,
      ],
      'unsafe methods with response (POST, PATCH): client requested format other than request body format when only JSON is allowed as a content type format' => [
        [
          'POST',
          'PATCH',
        ],
        [
          'json',
        ],
        [
          'xml',
        ],
        'xml',
        [
          'Content-Type' => 'application/json',
        ],
        $json_encoded,
        'xml',
        'text/xml',
        $xml_encoded,
      ],
    ];
    $unsafe_method_bodyless_test_cases = [
      'unsafe methods without response bodies (DELETE): client requested no format, response should have no format' => [
        [
          'DELETE',
        ],
        [
          'xml',
          'json',
        ],
        [
          'xml',
          'json',
        ],
        FALSE,
        [
          'Content-Type' => 'application/json',
        ],
        NULL,
        'xml',
        NULL,
        '',
      ],
      'unsafe methods without response bodies (DELETE): client requested format (XML), response should have no format' => [
        [
          'DELETE',
        ],
        [
          'xml',
          'json',
        ],
        [
          'xml',
          'json',
        ],
        'xml',
        [
          'Content-Type' => 'application/json',
        ],
        NULL,
        'xml',
        NULL,
        '',
      ],
      'unsafe methods without response bodies (DELETE): client requested format (JSON), response should have no format' => [
        [
          'DELETE',
        ],
        [
          'xml',
          'json',
        ],
        [
          'xml',
          'json',
        ],
        'json',
        [
          'Content-Type' => 'application/json',
        ],
        NULL,
        'json',
        NULL,
        '',
      ],
    ];
    return $safe_method_test_cases + $unsafe_method_bodied_test_cases + $unsafe_method_bodyless_test_cases;
  }

  /**
   * @return \Drupal\rest\EventSubscriber\ResourceResponseSubscriber
   */
  protected function getFunctioningResourceResponseSubscriber(RouteMatchInterface $route_match) {

    // Create a dummy of the renderer service.
    $renderer = $this
      ->prophesize(RendererInterface::class);
    $renderer
      ->executeInRenderContext(Argument::type(RenderContext::class), Argument::type('callable'))
      ->will(function ($args) {
      $callable = $args[1];
      return $callable();
    });

    // Instantiate the ResourceResponseSubscriber we will test.
    $resource_response_subscriber = new ResourceResponseSubscriber(new Serializer([], [
      new JsonEncoder(),
      new XmlEncoder(),
    ]), $renderer
      ->reveal(), $route_match);
    return $resource_response_subscriber;
  }

  /**
   * Generates route requirements based on supported formats.
   *
   * @param array $supported_response_formats
   *   The supported response formats to add to the route requirements.
   * @param array $supported_request_formats
   *   The supported request formats to add to the route requirements.
   *
   * @return array
   *   An array of route requirements.
   */
  protected function generateRouteRequirements(array $supported_response_formats, array $supported_request_formats) {
    $route_requirements = [
      '_format' => implode('|', $supported_response_formats),
    ];
    if (!empty($supported_request_formats)) {
      $route_requirements['_content_type_format'] = implode('|', $supported_request_formats);
    }
    return $route_requirements;
  }

}

Members

Namesort descending Modifiers Type Description Overrides
PhpUnitWarnings::$deprecationWarnings private static property Deprecation warnings from PHPUnit to raise with @trigger_error().
PhpUnitWarnings::addWarning public function Converts PHPUnit deprecation warnings to E_USER_DEPRECATED.
ResourceResponseSubscriberTest::generateRouteRequirements protected function Generates route requirements based on supported formats.
ResourceResponseSubscriberTest::getFunctioningResourceResponseSubscriber protected function
ResourceResponseSubscriberTest::providerTestResponseFormat public function
ResourceResponseSubscriberTest::providerTestSerialization public function
ResourceResponseSubscriberTest::testOnResponseWithCacheableResponse public function @covers ::onResponse @covers ::getResponseFormat @covers ::renderResponseBody @covers ::flattenResponse
ResourceResponseSubscriberTest::testOnResponseWithUncacheableResponse public function @covers ::onResponse @covers ::getResponseFormat @covers ::renderResponseBody @covers ::flattenResponse
ResourceResponseSubscriberTest::testResponseFormat public function @covers ::getResponseFormat
ResourceResponseSubscriberTest::testSerialization public function @covers ::onResponse @dataProvider providerTestSerialization
UnitTestCase::$randomGenerator protected property The random generator.
UnitTestCase::$root protected property The app root. 1
UnitTestCase::assertArrayEquals Deprecated protected function Asserts if two arrays are equal by sorting them first.
UnitTestCase::getClassResolverStub protected function Returns a stub class resolver.
UnitTestCase::getConfigFactoryStub public function Returns a stub config factory that behaves according to the passed array.
UnitTestCase::getConfigStorageStub public function Returns a stub config storage that returns the supplied configuration.
UnitTestCase::getContainerWithCacheTagsInvalidator protected function Sets up a container with a cache tags invalidator.
UnitTestCase::getRandomGenerator protected function Gets the random generator for the utility methods.
UnitTestCase::getStringTranslationStub public function Returns a stub translation manager that just returns the passed string.
UnitTestCase::randomMachineName public function Generates a unique random string containing letters and numbers.
UnitTestCase::setUp protected function 308
UnitTestCase::setUpBeforeClass public static function