View source
<?php
namespace Drupal\Tests\openapi\Functional;
use Drupal\field\Entity\FieldConfig;
use Drupal\field\Entity\FieldStorageConfig;
use Drupal\openapi_test\Entity\OpenApiTestEntityType;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\BrowserTestBase;
abstract class RequestTestBase extends BrowserTestBase {
protected $defaultTheme = 'stark';
protected static $generateExpectationFiles = FALSE;
const EXPECTED_STRUCTURE = [
'swagger' => 'swagger',
'info' => [
'description' => 'description',
'version' => 'version',
'title' => 'title',
],
'paths' => 'paths',
];
protected static $entityTestBundles = [
"taxonomy_term" => [
"camelids",
"taxonomy_term_test",
],
"openapi_test_entity" => [
"camelids",
"openapi_test_entity_test",
],
"openapi_test_entity_type" => [],
"user" => [],
];
public static $modules = [
'user',
'field',
'filter',
'text',
'taxonomy',
'serialization',
'openapi',
'openapi_test',
];
protected function setUp() {
parent::setUp();
foreach (static::$entityTestBundles['taxonomy_term'] as $bundle) {
if (!Vocabulary::load($bundle)) {
$vocabulary = Vocabulary::create([
'name' => $bundle,
'vid' => $bundle,
]);
$vocabulary
->save();
}
}
foreach (static::$entityTestBundles['openapi_test_entity'] as $bundle) {
if (!OpenApiTestEntityType::load($bundle)) {
OpenApiTestEntityType::create([
'label' => $bundle,
'id' => $bundle,
])
->save();
}
}
foreach (array_filter(static::$entityTestBundles) as $entity_type => $bundles) {
FieldStorageConfig::create([
'entity_type' => $entity_type,
'field_name' => 'field_test_' . $entity_type,
'type' => 'text',
])
->setCardinality(1)
->save();
foreach ($bundles as $bundle) {
FieldConfig::create([
'entity_type' => $entity_type,
'field_name' => 'field_test_' . $entity_type,
'bundle' => $bundle,
])
->setLabel('Test field')
->setTranslatable(FALSE)
->save();
}
}
$this
->drupalLogin($this
->drupalCreateUser([
'access openapi api docs',
'access content',
]));
}
public function testNotGenerating() {
$this
->assertFalse(static::$generateExpectationFiles, 'Expectation files generated. Change \\Drupal\\Tests\\openapi\\Functional\\RequestTest::$generateExpectationFiles to FALSE to run tests.');
}
public function providerRequestTypes() {
$data = [];
foreach (static::$entityTestBundles as $entity_type => $bundles) {
foreach ($bundles as $bundle) {
$data[static::API_MODULE . ':' . $entity_type . '_' . $bundle] = [
static::API_MODULE,
[
'entity_type_id' => $entity_type,
'bundle_name' => $bundle,
],
];
}
$data[static::API_MODULE . ':' . $entity_type] = [
static::API_MODULE,
[
'entity_type_id' => $entity_type,
],
];
}
$data[static::API_MODULE] = [
static::API_MODULE,
];
return $data;
}
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);
$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));
$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));
$this
->assertTrue(!empty($decoded_response['schemes']), 'Schema for ' . $api_module . ' should define at least one scheme.');
$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.');
$response_routeBase = substr($response_basePath, strlen($basePath));
$this
->assertEquals($this
->getRouteBase(), ltrim($response_routeBase, '/'), 'Route base path is invalid.');
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])) {
$this
->assertMimeType($decoded_response[$key], $options);
}
}
$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'] . '.');
}
}
}
$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') {
$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']);
}
$paths =& $decoded_response['paths'];
$tag_names = array_column($tags, 'name');
$all_method_tags = [];
foreach ($paths as $path => &$methods) {
foreach ($methods as $method => &$method_schema) {
$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));
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])) {
$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));
}
}
}
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");
$root_keys = [
'definitions',
'paths',
];
foreach (array_diff(array_keys($decoded_response), $root_keys) as $remove) {
unset($decoded_response[$remove]);
}
$file_name = $this
->buildExpectationsDirectory();
if ($options) {
$file_name .= "." . implode('.', $options);
}
$file_name .= '.json';
if (static::$generateExpectationFiles) {
$this
->saveExpectationFile($file_name, $decoded_response);
return;
}
$expected = json_decode(file_get_contents($file_name), TRUE);
$this
->nestedKsort($expected);
$this
->nestedKsort($decoded_response);
$this
->assertEquals($expected, $decoded_response, "The response does not match expected file: {$file_name}");
}
protected abstract function buildExpectationsDirectory();
private function saveExpectationFile($file_name, array $decoded_response) {
$re_encode = json_encode($decoded_response, JSON_PRETTY_PRINT);
file_put_contents($file_name, $re_encode);
}
protected abstract function getRouteBase();
protected abstract function assertMimeType(array $actual, array $options = []);
private function getBasePath() {
$path = rtrim(parse_url($this->baseUrl, PHP_URL_PATH), '/');
if (strlen($path) == 0) {
$path = "/";
}
elseif (substr($path, 0, 1) !== '/') {
$path = '/' . $path;
}
return $path;
}
private function nestedKsort(array &$array) {
if ($this
->isAssocArray($array)) {
ksort($array);
}
else {
usort($array, function ($a, $b) {
if (isset($a['name']) && isset($b['name'])) {
return strcmp($a['name'], $b['name']);
}
return -1;
});
}
foreach ($array as &$item) {
if (is_array($item)) {
$this
->nestedKsort($item);
}
}
}
private function isAssocArray(array $arr) {
if (empty($arr)) {
return FALSE;
}
return array_keys($arr) !== range(0, count($arr) - 1);
}
}