diff --git a/src/Normalizer/EntityReferenceFieldNormalizer.php b/src/Normalizer/EntityReferenceFieldNormalizer.php index 8b9dad2..baab98c 100644 --- a/src/Normalizer/EntityReferenceFieldNormalizer.php +++ b/src/Normalizer/EntityReferenceFieldNormalizer.php @@ -86,9 +86,10 @@ class EntityReferenceFieldNormalizer extends FieldNormalizer implements Denormal ->getFieldStorageDefinition() ->getCardinality(); $entity_collection = new EntityCollection(array_map(function ($item) { - if ($entity = $item->get('entity')->getValue()) { - return $this->entityRepository->getTranslationFromContext($entity); - } + // Get the referenced entity. + $entity = $item->get('entity')->getValue(); + // And get the translation in the requested language. + return $this->entityRepository->getTranslationFromContext($entity); }, (array) $field->getIterator())); $relationship = new Relationship($this->resourceTypeRepository, $field->getName(), $cardinality, $entity_collection, $field->getEntity(), $main_property); return $this->serializer->normalize($relationship, $format, $context); diff --git a/tests/src/Functional/JsonApiFunctionalBaseTest.php b/tests/src/Functional/JsonApiFunctionalBaseTest.php new file mode 100644 index 0000000..21664ab --- /dev/null +++ b/tests/src/Functional/JsonApiFunctionalBaseTest.php @@ -0,0 +1,280 @@ +httpClient = $this->container->get('http_client_factory') + ->fromOptions(['base_uri' => $this->baseUrl]); + + // Create Basic page and Article node types. + if ($this->profile != 'standard') { + $this->drupalCreateContentType(array( + 'type' => 'article', + 'name' => 'Article', + )); + + // Setup vocabulary. + Vocabulary::create([ + 'vid' => 'tags', + 'name' => 'Tags', + ])->save(); + + // Add tags and field_image to the article. + $this->createEntityReferenceField( + 'node', + 'article', + 'field_tags', + 'Tags', + 'taxonomy_term', + 'default', + [ + 'target_bundles' => [ + 'tags' => 'tags', + ], + 'auto_create' => TRUE, + ], + FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED + ); + $this->createImageField('field_image', 'article'); + } + + FieldStorageConfig::create(array( + 'field_name' => 'field_link', + 'entity_type' => 'node', + 'type' => 'link', + 'settings' => [], + 'cardinality' => 1, + ))->save(); + + $field_config = FieldConfig::create([ + 'field_name' => 'field_link', + 'label' => 'Link', + 'entity_type' => 'node', + 'bundle' => 'article', + 'required' => FALSE, + 'settings' => [], + 'description' => '', + ]); + $field_config->save(); + + $this->user = $this->drupalCreateUser([ + 'create article content', + 'edit any article content', + 'delete any article content', + ]); + + // Create a user that can + $this->userCanViewProfiles = $this->drupalCreateUser([ + 'access user profiles', + ]); + + $this->grantPermissions(Role::load(RoleInterface::ANONYMOUS_ID), [ + 'access user profiles', + 'administer taxonomy', + ]); + + drupal_flush_all_caches(); + } + + /** + * {@inheritdoc} + */ + protected function drupalGet($path, array $options = array(), array $headers = array()) { + // Make sure we don't forget the format parameter. + $options += ['query' => []]; + $options['query'] += ['_format' => 'api_json']; + + return parent::drupalGet($path, $options, $headers); + } + + /** + * Performs a HTTP request. Wraps the Guzzle HTTP client. + * + * Why wrap the Guzzle HTTP client? Because any error response is returned via + * an exception, which would make the tests unnecessarily complex to read. + * + * @see \GuzzleHttp\ClientInterface::request() + * + * @param string $method + * HTTP method. + * @param \Drupal\Core\Url $url + * URL to request. + * @param array $request_options + * Request options to apply. + * + * @return \Psr\Http\Message\ResponseInterface + */ + protected function request($method, Url $url, array $request_options) { + $url->setOption('query', ['_format' => 'api_json']); + try { + $response = $this->httpClient->request($method, $url->toString(), $request_options); + } + catch (ClientException $e) { + $response = $e->getResponse(); + } + catch (ServerException $e) { + $response = $e->getResponse(); + } + + return $response; + } + + /** + * Creates default content to test the API. + * + * @param int $num_articles + * Number of articles to create. + * @param int $num_tags + * Number of tags to create. + * @param bool $article_has_image + * Set to TRUE if you want to add an image to the generated articles. + * @param bool $article_has_link + * Set to TRUE if you want to add a link to the generated articles. + * @param bool $is_multilingual + * (optional) Set to TRUE if you want to enable multilingual content. + */ + protected function createDefaultContent($num_articles, $num_tags, $article_has_image, $article_has_link, $is_multilingual) { + $random = $this->getRandomGenerator(); + for ($created_tags = 0; $created_tags < $num_tags; $created_tags++) { + $term = Term::create([ + 'vid' => 'tags', + 'name' => $random->name(), + ]); + + if ($is_multilingual) { + $term->addTranslation('ca', ['name' => $term->getName() . ' (ca)']); + } + + $term->save(); + $this->tags[] = $term; + } + for ($created_nodes = 0; $created_nodes < $num_articles; $created_nodes++) { + // Get N random tags. + $selected_tags = mt_rand(1, $num_tags); + $tags = []; + while (count($tags) < $selected_tags) { + $tags[] = mt_rand(1, $num_tags); + $tags = array_unique($tags); + } + $values = [ + 'uid' => ['target_id' => $this->user->id()], + 'type' => 'article', + 'field_tags' => array_map(function ($tag) { + return ['target_id' => $tag]; + }, $tags), + ]; + if ($article_has_image) { + $file = File::create([ + 'uri' => 'vfs://' . $random->name() . '.png', + ]); + $file->setPermanent(); + $file->save(); + $this->files[] = $file; + $values['field_image'] = ['target_id' => $file->id(), 'alt' => 'alt text']; + } + if ($article_has_link) { + $values['field_link'] = [ + 'title' => $this->getRandomGenerator()->name(), + 'uri' => sprintf( + '%s://%s.%s', + 'http' . (mt_rand(0, 2) > 1 ? '' : 's'), + $this->getRandomGenerator()->name(), + 'org' + ), + ]; + } + $node = $this->createNode($values); + + if ($is_multilingual === static::IS_MULTILINGUAL) { + $values['title'] = $node->getTitle() . ' (ca)'; + $values['field_image']['alt'] = 'alt text (ca)'; + $node->addTranslation('ca', $values); + } + $node->save(); + + $this->nodes[] = $node; + } + if ($article_has_link) { + // Make sure that there is at least 1 https link for ::testRead() #19. + $this->nodes[0]->field_link = [ + 'title' => 'Drupal', + 'uri' => 'https://drupal.org' + ]; + $this->nodes[0]->save(); + } + } + +} diff --git a/tests/src/Functional/JsonApiFunctionalMultilingualTest.php b/tests/src/Functional/JsonApiFunctionalMultilingualTest.php new file mode 100644 index 0000000..3e27e4f --- /dev/null +++ b/tests/src/Functional/JsonApiFunctionalMultilingualTest.php @@ -0,0 +1,67 @@ +save(); + + // In order to reflect the changes for a multilingual site in the container + // we have to rebuild it. + $this->rebuildContainer(); + + \Drupal::configFactory()->getEditable('language.negotiation') + ->set('url.prefixes.ca', 'ca') + ->save(); + } + + /** + * Tests reading multilingual content. + */ + public function testReadMultilingual() { + $this->createDefaultContent(5, 5, TRUE, TRUE, static::IS_MULTILINGUAL); + + // Test reading an individual entity. + $output = Json::decode($this->drupalGet('/ca/jsonapi/node/article/' . $this->nodes[0]->uuid(), ['query' => ['include' => 'field_tags,field_image']])); + $this->assertEquals($this->nodes[0]->getTranslation('ca')->getTitle(), $output['data']['attributes']['title']); + + $included_tags = array_filter($output['included'], function ($entry) { + return $entry['type'] === 'taxonomy_term--tags'; + }); + $tag_name = $this->nodes[0]->get('field_tags')->entity + ->getTranslation('ca')->getName(); + // TODO figure out how to fetcht the alt text of an image. + $this->assertEquals($tag_name, reset($included_tags)['attributes']['name']); + + $output = Json::decode($this->drupalGet('/ca/jsonapi/node/article/' . $this->nodes[0]->uuid())); + $this->assertEquals($this->nodes[0]->getTranslation('ca')->getTitle(), $output['data']['attributes']['title']); + + // Test reading a collection of entities. + $output = Json::decode($this->drupalGet('/ca/jsonapi/node/article')); + $this->assertEquals($this->nodes[0]->getTranslation('ca')->getTitle(), $output['data'][0]['attributes']['title']); + } + +} diff --git a/tests/src/Functional/JsonApiFunctionalTest.php b/tests/src/Functional/JsonApiFunctionalTest.php index 8aaa779..2555d57 100644 --- a/tests/src/Functional/JsonApiFunctionalTest.php +++ b/tests/src/Functional/JsonApiFunctionalTest.php @@ -3,197 +3,13 @@ namespace Drupal\Tests\jsonapi\Functional; use Drupal\Component\Serialization\Json; -use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\Url; -use Drupal\field\Entity\FieldConfig; -use Drupal\field\Entity\FieldStorageConfig; -use Drupal\field\Tests\EntityReference\EntityReferenceTestTrait; -use Drupal\file\Entity\File; use Drupal\jsonapi\Routing\Param\OffsetPage; -use Drupal\language\Entity\ConfigurableLanguage; -use Drupal\taxonomy\Entity\Term; -use Drupal\taxonomy\Entity\Vocabulary; -use Drupal\Tests\BrowserTestBase; -use Drupal\Tests\image\Kernel\ImageFieldCreationTrait; -use Drupal\user\Entity\Role; -use Drupal\user\RoleInterface; -use GuzzleHttp\Exception\ClientException; -use GuzzleHttp\Exception\ServerException; /** * @group jsonapi */ -class JsonApiFunctionalTest extends BrowserTestBase { - - use EntityReferenceTestTrait; - use ImageFieldCreationTrait; - - const IS_MULTILINGUAL = TRUE; - const IS_NOT_MULTILINGUAL = FALSE; - - public static $modules = [ - 'basic_auth', - 'jsonapi', - 'serialization', - 'node', - 'image', - 'taxonomy', - 'link', - ]; - - /** - * @var \Drupal\user\Entity\User - */ - protected $user; - - /** - * @var \Drupal\user\Entity\User - */ - protected $userCanViewProfiles; - - /** - * @var \Drupal\node\Entity\Node[] - */ - protected $nodes = []; - - /** - * @var \Drupal\taxonomy\Entity\Term[] - */ - protected $tags = []; - - /** - * @var \Drupal\file\Entity\File[] - */ - protected $files = []; - - /** - * @var \GuzzleHttp\ClientInterface - */ - protected $httpClient; - - - /** - * {@inheritdoc} - */ - protected function setUp() { - parent::setUp(); - - // Set up a HTTP client that accepts relative URLs. - $this->httpClient = $this->container->get('http_client_factory') - ->fromOptions(['base_uri' => $this->baseUrl]); - - // Create Basic page and Article node types. - if ($this->profile != 'standard') { - $this->drupalCreateContentType(array( - 'type' => 'article', - 'name' => 'Article', - )); - - // Setup vocabulary. - Vocabulary::create([ - 'vid' => 'tags', - 'name' => 'Tags', - ])->save(); - - // Add tags and field_image to the article. - $this->createEntityReferenceField( - 'node', - 'article', - 'field_tags', - 'Tags', - 'taxonomy_term', - 'default', - [ - 'target_bundles' => [ - 'tags' => 'tags', - ], - 'auto_create' => TRUE, - ], - FieldStorageDefinitionInterface::CARDINALITY_UNLIMITED - ); - $this->createImageField('field_image', 'article'); - } - - FieldStorageConfig::create(array( - 'field_name' => 'field_link', - 'entity_type' => 'node', - 'type' => 'link', - 'settings' => [], - 'cardinality' => 1, - ))->save(); - - $field_config = FieldConfig::create([ - 'field_name' => 'field_link', - 'label' => 'Link', - 'entity_type' => 'node', - 'bundle' => 'article', - 'required' => FALSE, - 'settings' => [], - 'description' => '', - ]); - $field_config->save(); - - $this->user = $this->drupalCreateUser([ - 'create article content', - 'edit any article content', - 'delete any article content', - ]); - - // Create a user that can - $this->userCanViewProfiles = $this->drupalCreateUser([ - 'access user profiles', - ]); - - $this->grantPermissions(Role::load(RoleInterface::ANONYMOUS_ID), [ - 'access user profiles', - 'administer taxonomy', - ]); - - drupal_flush_all_caches(); - } - - /** - * {@inheritdoc} - */ - protected function drupalGet($path, array $options = array(), array $headers = array()) { - // Make sure we don't forget the format parameter. - $options += ['query' => []]; - $options['query'] += ['_format' => 'api_json']; - - return parent::drupalGet($path, $options, $headers); - } - - /** - * Performs a HTTP request. Wraps the Guzzle HTTP client. - * - * Why wrap the Guzzle HTTP client? Because any error response is returned via - * an exception, which would make the tests unnecessarily complex to read. - * - * @see \GuzzleHttp\ClientInterface::request() - * - * @param string $method - * HTTP method. - * @param \Drupal\Core\Url $url - * URL to request. - * @param array $request_options - * Request options to apply. - * - * @return \Psr\Http\Message\ResponseInterface - */ - protected function request($method, Url $url, array $request_options) { - $url->setOption('query', ['_format' => 'api_json']); - try { - $response = $this->httpClient->request($method, $url->toString(), $request_options); - } - catch (ClientException $e) { - $response = $e->getResponse(); - } - catch (ServerException $e) { - $response = $e->getResponse(); - } - - return $response; - } +class JsonApiFunctionalTest extends JsonApiFunctionalBaseTest { /** * Test the GET method. @@ -418,34 +234,6 @@ class JsonApiFunctionalTest extends BrowserTestBase { } /** - * Tests reading multilingual content. - */ - public function testReadMultilingual() { - $this->setupMultilingual(); - - $this->createDefaultContent(5, 5, TRUE, TRUE, static::IS_MULTILINGUAL); - - // Test reading an individual entity. - $output = Json::decode($this->drupalGet('/es/jsonapi/node/article/' . $this->nodes[0]->uuid(), ['query' => ['include' => 'field_tags,field_image']])); - $this->assertEquals($this->nodes[0]->getTranslation('es')->getTitle(), $output['data']['attributes']['title']); - - $included_tags = array_filter($output['included'], function ($entry) { - return $entry['type'] === 'taxonomy_term--tags'; - }); - $this->assertEquals($this->nodes[0]->get('field_tags')->entity->getTranslation('es')->getName(), reset($included_tags)['attributes']['name']); - // @todo figure out how to fetcht the alt text of an image. - - $output = Json::decode($this->drupalGet('/es/jsonapi/node/article/' . $this->nodes[0]->uuid())); - $this->assertEquals($this->nodes[0]->getTranslation('es')->getTitle(), $output['data']['attributes']['title']); - - // Test reading a collection of entities. - - $output = Json::decode($this->drupalGet('/es/jsonapi/node/article')); - $this->assertEquals($this->nodes[0]->getTranslation('es')->getTitle(), $output['data'][0]['attributes']['title']); - } - - - /** * Test POST, PATCH and DELETE. */ public function testWrite() { @@ -728,106 +516,4 @@ class JsonApiFunctionalTest extends BrowserTestBase { $this->assertEquals(404, $response->getStatusCode()); } - /** - * Creates default content to test the API. - * - * @param int $num_articles - * Number of articles to create. - * @param int $num_tags - * Number of tags to create. - * @param bool $article_has_image - * Set to TRUE if you want to add an image to the generated articles. - * @param bool $article_has_link - * Set to TRUE if you want to add a link to the generated articles. - * @param bool $is_multilingual - * (optional) Set to TRUE if you want to enable multilingual content. - */ - protected function createDefaultContent($num_articles, $num_tags, $article_has_image, $article_has_link, $is_multilingual) { - $random = $this->getRandomGenerator(); - for ($created_tags = 0; $created_tags < $num_tags; $created_tags++) { - $term = Term::create([ - 'vid' => 'tags', - 'name' => $random->name(), - ]); - - if ($is_multilingual) { - $term->addTranslation('es', ['name' => $term->getName() . ' (es)']); - } - - $term->save(); - $this->tags[] = $term; - } - for ($created_nodes = 0; $created_nodes < $num_articles; $created_nodes++) { - // Get N random tags. - $selected_tags = mt_rand(1, $num_tags); - $tags = []; - while (count($tags) < $selected_tags) { - $tags[] = mt_rand(1, $num_tags); - $tags = array_unique($tags); - } - $values = [ - 'uid' => ['target_id' => $this->user->id()], - 'type' => 'article', - 'field_tags' => array_map(function ($tag) { - return ['target_id' => $tag]; - }, $tags), - ]; - if ($article_has_image) { - $file = File::create([ - 'uri' => 'vfs://' . $random->name() . '.png', - ]); - $file->setPermanent(); - $file->save(); - $this->files[] = $file; - $values['field_image'] = ['target_id' => $file->id(), 'alt' => 'alt text']; - } - if ($article_has_link) { - $values['field_link'] = [ - 'title' => $this->getRandomGenerator()->name(), - 'uri' => sprintf( - '%s://%s.%s', - 'http' . (mt_rand(0, 2) > 1 ? '' : 's'), - $this->getRandomGenerator()->name(), - 'org' - ), - ]; - } - $node = $this->createNode($values); - - if ($is_multilingual === static::IS_MULTILINGUAL) { - $values['title'] = $node->getTitle() . ' (es)'; - $values['field_image']['alt'] = 'alt text (es)'; - $node->addTranslation('es', $values); - } - $node->save(); - - $this->nodes[] = $node; - } - if ($article_has_link) { - // Make sure that there is at least 1 https link for ::testRead() #19. - $this->nodes[0]->field_link = [ - 'title' => 'Drupal', - 'uri' => 'https://drupal.org' - ]; - $this->nodes[0]->save(); - } - } - - /** - * Setup the multilingual configuration. - */ - protected function setupMultilingual() { - $this->container->get('module_installer')->install(['language']); - $language = ConfigurableLanguage::createFromLangcode('es'); - $language->save(); - - // In order to reflect the changes for a multilingual site in the container - // we have to rebuild it. - $this->rebuildContainer(); - - \Drupal::configFactory()->getEditable('language.negotiation') - ->set('url.prefixes.es', 'es') - ->save(); - } - }