You are here

public function EntityResourceTestBase::testPost in Drupal 8

Same name and namespace in other branches
  1. 9 core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testPost()
  2. 10 core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php \Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase::testPost()

Tests a POST request for an entity, plus edge cases to ensure good DX.

1 call to EntityResourceTestBase::testPost()
MediaResourceTestBase::testPost in core/modules/media/tests/src/Functional/Rest/MediaResourceTestBase.php
Tests a POST request for an entity, plus edge cases to ensure good DX.
2 methods override EntityResourceTestBase::testPost()
FileResourceTestBase::testPost in core/modules/file/tests/src/Functional/Rest/FileResourceTestBase.php
Tests a POST request for an entity, plus edge cases to ensure good DX.
MediaResourceTestBase::testPost in core/modules/media/tests/src/Functional/Rest/MediaResourceTestBase.php
Tests a POST request for an entity, plus edge cases to ensure good DX.

File

core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php, line 819

Class

EntityResourceTestBase
Even though there is the generic EntityResource, it's necessary for every entity type to have its own test, because they each have different fields, validation constraints, et cetera. It's not because the generic case works, that every case…

Namespace

Drupal\Tests\rest\Functional\EntityResource

Code

public function testPost() {

  // @todo Remove this in https://www.drupal.org/node/2300677.
  if ($this->entity instanceof ConfigEntityInterface) {
    $this
      ->assertTrue(TRUE, 'POSTing config entities is not yet supported.');
    return;
  }
  $this
    ->initAuthentication();
  $has_canonical_url = $this->entity
    ->hasLinkTemplate('canonical');

  // Try with all of the following request bodies.
  $unparseable_request_body = '!{>}<';
  $parseable_valid_request_body = $this->serializer
    ->encode($this
    ->getNormalizedPostEntity(), static::$format);
  $parseable_valid_request_body_2 = $this->serializer
    ->encode($this
    ->getSecondNormalizedPostEntity(), static::$format);
  $parseable_invalid_request_body = $this->serializer
    ->encode($this
    ->makeNormalizationInvalid($this
    ->getNormalizedPostEntity(), 'label'), static::$format);
  $parseable_invalid_request_body_2 = $this->serializer
    ->encode($this
    ->getNormalizedPostEntity() + [
    'uuid' => [
      $this
        ->randomMachineName(129),
    ],
  ], static::$format);
  $parseable_invalid_request_body_3 = $this->serializer
    ->encode($this
    ->getNormalizedPostEntity() + [
    'field_rest_test' => [
      [
        'value' => $this
          ->randomString(),
      ],
    ],
  ], static::$format);

  // The URL and Guzzle request options that will be used in this test. The
  // request options will be modified/expanded throughout this test:
  // - to first test all mistakes a developer might make, and assert that the
  //   error responses provide a good DX
  // - to eventually result in a well-formed request that succeeds.
  $url = $this
    ->getEntityResourcePostUrl();
  $request_options = [];

  // DX: 404 when resource not provisioned. HTML response because missing
  // ?_format query string.
  $response = $this
    ->request('POST', $url, $request_options);
  $this
    ->assertSame(404, $response
    ->getStatusCode());
  $this
    ->assertSame([
    'text/html; charset=UTF-8',
  ], $response
    ->getHeader('Content-Type'));
  $url
    ->setOption('query', [
    '_format' => static::$format,
  ]);

  // DX: 404 when resource not provisioned.
  $response = $this
    ->request('POST', $url, $request_options);
  $this
    ->assertResourceErrorResponse(404, 'No route found for "POST ' . str_replace($this->baseUrl, '', $this
    ->getEntityResourcePostUrl()
    ->setAbsolute()
    ->toString()) . '"', $response);
  $this
    ->provisionEntityResource();

  // Simulate the developer again forgetting the ?_format query string.
  $url
    ->setOption('query', []);

  // DX: 415 when no Content-Type request header. HTML response because
  // missing ?_format query string.
  $response = $this
    ->request('POST', $url, $request_options);
  $this
    ->assertSame(415, $response
    ->getStatusCode());
  $this
    ->assertSame([
    'text/html; charset=UTF-8',
  ], $response
    ->getHeader('Content-Type'));
  $this
    ->assertStringContainsString('A client error happened', (string) $response
    ->getBody());
  $url
    ->setOption('query', [
    '_format' => static::$format,
  ]);

  // DX: 415 when no Content-Type request header.
  $response = $this
    ->request('POST', $url, $request_options);
  $this
    ->assertResourceErrorResponse(415, 'No "Content-Type" request header specified', $response);
  $request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;
  if (static::$auth) {

    // DX: forgetting authentication: authentication provider-specific error
    // response.
    $response = $this
      ->request('POST', $url, $request_options);
    $this
      ->assertResponseWhenMissingAuthentication('POST', $response);
  }
  $request_options = NestedArray::mergeDeep($request_options, $this
    ->getAuthenticationRequestOptions('POST'));

  // DX: 403 when unauthorized.
  $response = $this
    ->request('POST', $url, $request_options);
  $this
    ->assertResourceErrorResponse(403, $this
    ->getExpectedUnauthorizedAccessMessage('POST'), $response);
  $this
    ->setUpAuthorization('POST');

  // DX: 400 when no request body.
  $response = $this
    ->request('POST', $url, $request_options);
  $this
    ->assertResourceErrorResponse(400, 'No entity content received.', $response);
  $request_options[RequestOptions::BODY] = $unparseable_request_body;

  // DX: 400 when unparseable request body.
  $response = $this
    ->request('POST', $url, $request_options);
  $this
    ->assertResourceErrorResponse(400, 'Syntax error', $response);
  $request_options[RequestOptions::BODY] = $parseable_invalid_request_body;

  // DX: 422 when invalid entity: multiple values sent for single-value field.
  $response = $this
    ->request('POST', $url, $request_options);
  if ($label_field = $this->entity
    ->getEntityType()
    ->hasKey('label') ? $this->entity
    ->getEntityType()
    ->getKey('label') : static::$labelFieldName) {
    $label_field_capitalized = $this->entity
      ->getFieldDefinition($label_field)
      ->getLabel();
    $this
      ->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\n{$label_field}: {$label_field_capitalized}: this field cannot hold more than 1 values.\n", $response);
  }
  $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2;

  // DX: 422 when invalid entity: UUID field too long.
  // @todo Fix this in https://www.drupal.org/node/2149851.
  if ($this->entity
    ->getEntityType()
    ->hasKey('uuid')) {
    $response = $this
      ->request('POST', $url, $request_options);
    $this
      ->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nuuid.0.value: UUID: may not be longer than 128 characters.\n", $response);
  }
  $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_3;

  // DX: 403 when entity contains field without 'edit' access.
  $response = $this
    ->request('POST', $url, $request_options);
  $this
    ->assertResourceErrorResponse(403, "Access denied on creating field 'field_rest_test'.", $response);
  $request_options[RequestOptions::BODY] = $parseable_valid_request_body;

  // Before sending a well-formed request, allow the normalization and
  // authentication provider edge cases to also be tested.
  $this
    ->assertNormalizationEdgeCases('POST', $url, $request_options);
  $this
    ->assertAuthenticationEdgeCases('POST', $url, $request_options);
  $request_options[RequestOptions::HEADERS]['Content-Type'] = 'text/xml';

  // DX: 415 when request body in existing but not allowed format.
  $response = $this
    ->request('POST', $url, $request_options);
  $this
    ->assertResourceErrorResponse(415, 'No route found that matches "Content-Type: text/xml"', $response);
  $request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;

  // 201 for well-formed request.
  $response = $this
    ->request('POST', $url, $request_options);
  $this
    ->assertResourceResponse(201, FALSE, $response);
  if ($has_canonical_url) {
    $location = $this->entityStorage
      ->load(static::$firstCreatedEntityId)
      ->toUrl('canonical')
      ->setAbsolute(TRUE)
      ->toString();
    $this
      ->assertSame([
      $location,
    ], $response
      ->getHeader('Location'));
  }
  else {
    $this
      ->assertSame([], $response
      ->getHeader('Location'));
  }
  $this
    ->assertFalse($response
    ->hasHeader('X-Drupal-Cache'));

  // If the entity is stored, perform extra checks.
  if (get_class($this->entityStorage) !== ContentEntityNullStorage::class) {

    // Assert that the entity was indeed created, and that the response body
    // contains the serialized created entity.
    $created_entity = $this->entityStorage
      ->loadUnchanged(static::$firstCreatedEntityId);
    $created_entity_normalization = $this->serializer
      ->normalize($created_entity, static::$format, [
      'account' => $this->account,
    ]);
    $this
      ->assertSame($created_entity_normalization, $this->serializer
      ->decode((string) $response
      ->getBody(), static::$format));
    $this
      ->assertStoredEntityMatchesSentNormalization($this
      ->getNormalizedPostEntity(), $created_entity);
  }
  $this
    ->config('rest.settings')
    ->set('bc_entity_resource_permissions', TRUE)
    ->save(TRUE);
  $this
    ->refreshTestStateAfterRestConfigChange();
  $request_options[RequestOptions::BODY] = $parseable_valid_request_body_2;

  // DX: 403 when unauthorized.
  $response = $this
    ->request('POST', $url, $request_options);
  $this
    ->assertResourceErrorResponse(403, $this
    ->getExpectedUnauthorizedAccessMessage('POST'), $response);
  $this
    ->grantPermissionsToTestedRole([
    'restful post entity:' . static::$entityTypeId,
  ]);

  // 201 for well-formed request.
  // If the entity is stored, delete the first created entity (in case there
  // is a uniqueness constraint).
  if (get_class($this->entityStorage) !== ContentEntityNullStorage::class) {
    $this->entityStorage
      ->load(static::$firstCreatedEntityId)
      ->delete();
  }
  $response = $this
    ->request('POST', $url, $request_options);
  $this
    ->assertResourceResponse(201, FALSE, $response);
  $created_entity = $this->entityStorage
    ->load(static::$secondCreatedEntityId);
  if ($has_canonical_url) {
    $location = $created_entity
      ->toUrl('canonical')
      ->setAbsolute(TRUE)
      ->toString();
    $this
      ->assertSame([
      $location,
    ], $response
      ->getHeader('Location'));
  }
  else {
    $this
      ->assertSame([], $response
      ->getHeader('Location'));
  }
  $this
    ->assertFalse($response
    ->hasHeader('X-Drupal-Cache'));
  if ($this->entity
    ->getEntityType()
    ->getStorageClass() !== ContentEntityNullStorage::class && $this->entity
    ->getEntityType()
    ->hasKey('uuid')) {

    // 500 when creating an entity with a duplicate UUID.
    $normalized_entity = $this
      ->getModifiedEntityForPostTesting();
    $normalized_entity[$created_entity
      ->getEntityType()
      ->getKey('uuid')] = [
      [
        'value' => $created_entity
          ->uuid(),
      ],
    ];
    if ($label_field) {
      $normalized_entity[$label_field] = [
        [
          'value' => $this
            ->randomMachineName(),
        ],
      ];
    }
    $request_options[RequestOptions::BODY] = $this->serializer
      ->encode($normalized_entity, static::$format);
    $response = $this
      ->request('POST', $url, $request_options);
    $this
      ->assertSame(500, $response
      ->getStatusCode());
    $this
      ->assertStringContainsString('Internal Server Error', (string) $response
      ->getBody());

    // 201 when successfully creating an entity with a new UUID.
    $normalized_entity = $this
      ->getModifiedEntityForPostTesting();
    $new_uuid = \Drupal::service('uuid')
      ->generate();
    $normalized_entity[$created_entity
      ->getEntityType()
      ->getKey('uuid')] = [
      [
        'value' => $new_uuid,
      ],
    ];
    if ($label_field) {
      $normalized_entity[$label_field] = [
        [
          'value' => $this
            ->randomMachineName(),
        ],
      ];
    }
    $request_options[RequestOptions::BODY] = $this->serializer
      ->encode($normalized_entity, static::$format);
    $response = $this
      ->request('POST', $url, $request_options);
    $this
      ->assertResourceResponse(201, FALSE, $response);
    $entities = $this->entityStorage
      ->loadByProperties([
      $created_entity
        ->getEntityType()
        ->getKey('uuid') => $new_uuid,
    ]);
    $new_entity = reset($entities);
    $this
      ->assertNotNull($new_entity);
    $new_entity
      ->delete();
  }

  // BC: old default POST URLs have their path updated by the inbound path
  // processor \Drupal\rest\PathProcessor\PathProcessorEntityResourceBC to the
  // new URL, which is derived from the 'create' link template if an entity
  // type specifies it.
  if ($this->entity
    ->getEntityType()
    ->hasLinkTemplate('create')) {
    $this->entityStorage
      ->load(static::$secondCreatedEntityId)
      ->delete();
    $old_url = Url::fromUri('base:entity/' . static::$entityTypeId);
    $old_url
      ->setOption('query', [
      '_format' => static::$format,
    ]);
    $response = $this
      ->request('POST', $old_url, $request_options);
    $this
      ->assertResourceResponse(201, FALSE, $response);
  }
}