You are here

protected function RequestTest::requestOpenApiJson in OpenAPI 8

Makes OpenAPI request and checks the response.

Parameters

string $api_module: The API module being tested. Either 'rest' or 'jsonapi'.

array $options: The query options for generating the OpenAPI output.

1 call to RequestTest::requestOpenApiJson()
RequestTest::testRequests in tests/src/Functional/RequestTest.php
Tests OpenAPI requests.

File

tests/src/Functional/RequestTest.php, line 211

Class

RequestTest
Tests requests OpenAPI routes.

Namespace

Drupal\Tests\openapi\Functional

Code

protected function requestOpenApiJson($api_module, array $options = []) {
  $get_options = [
    'query' => [
      '_format' => 'json',
      'options' => $options,
    ],
  ];
  $response = $this
    ->drupalGet("openapi/{$api_module}", $get_options);
  $decoded_response = json_decode($response, TRUE);
  $this
    ->assertSession()
    ->statusCodeEquals(200);

  // Test the the first tier schema has the expected keys.
  $structure_keys = array_keys(static::EXPECTED_STRUCTURE);
  $response_keys = array_keys($decoded_response);
  $missing = array_diff($structure_keys, $response_keys);
  $this
    ->assertTrue(empty($missing), 'Schema missing expected key(s): ' . implode(', ', $missing));

  // Test that the required info block keys exist in the response.
  $structure_info_keys = array_keys(static::EXPECTED_STRUCTURE['info']);
  $response_keys = array_keys($decoded_response['info']);
  $missing_info = array_diff($structure_info_keys, $response_keys);
  $this
    ->assertTrue(empty($missing_info), 'Schema info missing expected key(s): ' . implode(', ', $missing_info));

  // Test that schemes is not empty.
  $this
    ->assertTrue(!empty($decoded_response['schemes']), 'Schema for ' . $api_module . ' should define at least one scheme.');

  // Test basePath and host.
  $port = parse_url($this->baseUrl, PHP_URL_PORT);
  $host = parse_url($this->baseUrl, PHP_URL_HOST) . ($port ? ':' . $port : '');
  $this
    ->assertEquals($host, $decoded_response['host'], 'Schema has invalid host.');
  $basePath = $this
    ->getBasePath();
  $response_basePath = $decoded_response['basePath'];
  $this
    ->assertEquals($basePath, substr($response_basePath, 0, strlen($basePath)), 'Schema has invalid basePath.');
  $routeBase = $api_module === 'jsonapi' ? 'jsonapi' : '';
  $response_routeBase = substr($response_basePath, strlen($basePath));

  // Verify that with the subdirectory removed, that the basePath is correct.
  $this
    ->assertEquals($routeBase, ltrim($response_routeBase, '/'), 'Route base path is invalid.');

  // Verify that root consumes and produces exists and is not empty.
  foreach ([
    'consumes',
    'produces',
  ] as $key) {
    $this
      ->assertArrayHasKey($key, $decoded_response, "Schema does not contains a root {$key}");
    $this
      ->assertNotEmpty($decoded_response[$key], "Schema has empty root {$key}");
    if (!isset($decoded_response[$key])) {
      if ($api_module == 'jsonapi') {
        $this
          ->assertEquals([
          'application/vnd.api+json',
        ], $decoded_response[$key], "{$api_module} root {$key} should only contain application/vnd.api+json");
      }
      elseif ($api_module == 'rest') {
        $rest_mimetypes = [
          'application/json',
        ];
        if (isset($options['entity_type_id']) && $options['entity_type_id'] === 'openapi_test_entity') {
          $rest_mimetypes[] = 'application/hal+json';
        }
        $this
          ->assertEquals($rest_mimetypes, $decoded_response[$key], "{$api_module} root {$key} should only contain " . implode(' and ', $rest_mimetypes));
      }
    }
  }

  /*
   * Tags for rest schema define 'x-entity-type' to reference the entity type
   * associated with the entity. This value should exist in the definitions.
   *
   * NOTE: Currently not all entity types are provided as definitions. As a
   * result, the below test is subject to failure, and has been disabled.
   *
   * @TODO: #2940397 - Convert x-entity-type to x-definition.
   * @TODO: #2940407 - Provide all entity types as definitions.
   */
  $tags = $decoded_response['tags'];
  if (FALSE) {
    $definitions = $decoded_response['definitions'];
    foreach ($tags as $tag) {
      if (isset($tag['x-entity-type'])) {
        $type_id = $tag['x-entity-type'];
        $this
          ->assertTrue(isset($definitions[$type_id]), 'The \'x-entity-type\' ' . $type_id . ' is invalid for ' . $tag['name'] . '.');
      }
    }
  }

  // Validate that all security definitions are valid, and have a provider.
  $security_definitions = $decoded_response['securityDefinitions'];
  $auth_providers = $this->container
    ->get('authentication_collector')
    ->getSortedProviders();
  $supported_security_types = [
    'basic',
    'apiKey',
    'cookie',
    'oauth',
    'oauth2',
  ];
  foreach ($security_definitions as $definition_id => $definition) {
    if ($definition_id !== 'csrf_token') {

      // CSRF Token will never have an auth collector, all others shoud.
      $this
        ->assertTrue(array_key_exists($definition_id, $auth_providers), 'Security definition ' . $definition_id . ' not an auth collector.');
    }
    $this
      ->assertTrue(in_array($definition['type'], $supported_security_types), 'Security definition schema ' . $definition_id . ' has invalid type ' . $definition['type']);
  }

  // Test paths for valid tags, schema, security, and definitions.
  $paths =& $decoded_response['paths'];
  $tag_names = array_column($tags, 'name');
  $all_method_tags = [];
  foreach ($paths as $path => &$methods) {
    foreach ($methods as $method => &$method_schema) {

      // Ensure all tags are defined.
      $missing_tags = array_diff($method_schema['tags'], $tag_names);
      $all_method_tags = array_merge($all_method_tags, $method_schema['tags']);
      $this
        ->assertTrue(empty($missing_tags), 'Method ' . $method . ' for ' . $path . ' has invalid tag(s): ' . implode(', ', $missing_tags));

      // Ensure all request schemes are defined.
      if (isset($method_schema['schemes'])) {
        $missing_schemas = array_diff($method_schema['schemes'], $decoded_response['schemes']);
        $this
          ->assertTrue(empty($missing_schemas), 'Method ' . $method . ' for ' . $path . ' has invalid scheme(s): ' . implode(', ', $missing_schemas));
      }
      $response_security_types = array_keys($decoded_response['securityDefinitions']);
      if (isset($method_schema['security'])) {
        foreach ($method_schema['security'] as $security_definitions) {
          $security_types = array_keys($security_definitions);
          $missing_security_types = array_diff($security_types, $response_security_types);
          $this
            ->assertTrue(empty($missing_security_types), 'Method ' . $method . ' for ' . $path . ' has invalid security type(s): ' . implode(', ', $missing_security_types) . ' + ' . implode(', ', $security_types) . ' + ' . implode(', ', $response_security_types));
        }
      }
      foreach ([
        'consumes',
        'produces',
      ] as $key) {
        if (isset($method_schema[$key]) && !empty($method_schema[$key])) {

          // Filter out mimetypes that exist in parent.
          $method_extra_mimetypes = array_diff($method_schema[$key], $decoded_response[$key]);
          $this
            ->assertEmpty($method_extra_mimetypes, 'Method ' . $method . ' for ' . $path . ' has invalid mime type(s): ' . implode(', ', $method_extra_mimetypes));
          if ($api_module == 'rest') {
            $rest_mimetypes = [
              'application/json',
            ];
            if (isset($options['entity_type_id']) && $options['entity_type_id'] === 'openapi_test_entity') {
              $rest_mimetypes[] = 'application/hal+json';
            }
            $this
              ->assertEquals($rest_mimetypes, $method_schema[$key], 'Entity type ' . $options['entity_type_id'] . ' should only have REST mimetype(s): ' . implode(', ', $rest_mimetypes));
          }
        }
      }

      // Remove all tested properties from method schema.
      unset($method_schema['tags']);
      unset($method_schema['schemes']);
      unset($method_schema['security']);
    }
  }
  $all_method_tags = array_unique($all_method_tags);
  asort($all_method_tags);
  asort($tag_names);
  $this
    ->assertEquals(array_values($all_method_tags), array_values($tag_names), "Method tags equal tag names");

  // Strip response down to only untested properties.
  $root_keys = [
    'definitions',
    'paths',
  ];
  foreach (array_diff(array_keys($decoded_response), $root_keys) as $remove) {
    unset($decoded_response[$remove]);
  }

  // Build file name.
  $file_name = __DIR__ . "/../../expectations/{$api_module}";
  if ($options) {
    $file_name .= "." . implode('.', $options);
  }
  $file_name .= '.json';
  if (static::GENERATE_EXPECTATION_FILES) {
    $this
      ->saveExpectationFile($file_name, $decoded_response);

    // Response assertion is not performed when generating expectation
    // files.
    return;
  }

  // Load expected value and test remaining schema.
  $expected = json_decode(file_get_contents($file_name), TRUE);
  $this
    ->nestedKsort($expected);
  $this
    ->nestedKsort($decoded_response);

  // @todo Update the expectations to include path alias stuff, and then
  // delete these unset() calls.
  unset($decoded_response['definitions']['path_alias--path_alias']);
  unset($decoded_response['paths']['/path_alias/path_alias']);
  unset($decoded_response['paths']['/path_alias/path_alias/{entity}']);
  $this
    ->assertEquals($expected, $decoded_response, "The response does not match expected file: {$file_name}");
}