You are here

public function ResourceTestBase::testPostIndividual in JSON:API 8

Same name and namespace in other branches
  1. 8.2 tests/src/Functional/ResourceTestBase.php \Drupal\Tests\jsonapi\Functional\ResourceTestBase::testPostIndividual()

Tests POSTing an individual resource, plus edge cases to ensure good DX.

1 call to ResourceTestBase::testPostIndividual()
MediaTest::testPostIndividual in tests/src/Functional/MediaTest.php
Tests POSTing an individual resource, plus edge cases to ensure good DX.
5 methods override ResourceTestBase::testPostIndividual()
BlockContentTest::testPostIndividual in tests/src/Functional/BlockContentTest.php
Tests POSTing an individual resource, plus edge cases to ensure good DX.
FileTest::testPostIndividual in tests/src/Functional/FileTest.php
Tests POSTing an individual resource, plus edge cases to ensure good DX.
ItemTest::testPostIndividual in tests/src/Functional/ItemTest.php
Tests POSTing an individual resource, plus edge cases to ensure good DX.
MediaTest::testPostIndividual in tests/src/Functional/MediaTest.php
Tests POSTing an individual resource, plus edge cases to ensure good DX.
ShortcutTest::testPostIndividual in tests/src/Functional/ShortcutTest.php
Tests POSTing an individual resource, plus edge cases to ensure good DX.

File

tests/src/Functional/ResourceTestBase.php, line 1794

Class

ResourceTestBase
Subclass this for every JSON API resource type.

Namespace

Drupal\Tests\jsonapi\Functional

Code

public function testPostIndividual() {

  // @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;
  }

  // Try with all of the following request bodies.
  $unparseable_request_body = '!{>}<';
  $parseable_valid_request_body = Json::encode($this
    ->getPostDocument());

  /* $parseable_valid_request_body_2 = Json::encode($this->getNormalizedPostEntity()); */
  $parseable_invalid_request_body_missing_type = Json::encode($this
    ->removeResourceTypeFromDocument($this
    ->getPostDocument(), 'type'));
  $parseable_invalid_request_body = Json::encode($this
    ->makeNormalizationInvalid($this
    ->getPostDocument(), 'label'));
  $parseable_invalid_request_body_2 = Json::encode(NestedArray::mergeDeep([
    'data' => [
      'id' => $this
        ->randomMachineName(129),
    ],
  ], $this
    ->getPostDocument()));
  $parseable_invalid_request_body_3 = Json::encode(NestedArray::mergeDeep([
    'data' => [
      'attributes' => [
        'field_rest_test' => $this
          ->randomString(),
      ],
    ],
  ], $this
    ->getPostDocument()));
  $parseable_invalid_request_body_4 = Json::encode(NestedArray::mergeDeep([
    'data' => [
      'attributes' => [
        'field_nonexistent' => $this
          ->randomString(),
      ],
    ],
  ], $this
    ->getPostDocument()));

  // 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 = Url::fromRoute(sprintf('jsonapi.%s.collection', static::$resourceTypeName));
  $request_options = [];
  $request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
  $request_options = NestedArray::mergeDeep($request_options, $this
    ->getAuthenticationRequestOptions());

  // @todo Uncomment in https://www.drupal.org/project/jsonapi/issues/2934149.
  // @codingStandardsIgnoreStart

  /*
      // 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->assertContains('A client error happened', (string) $response->getBody());

      $url->setOption('query', ['_format' => 'api_json']);

      // 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);
  */

  // @codingStandardsIgnoreEnd
  $request_options[RequestOptions::HEADERS]['Content-Type'] = '';

  // DX: 400 when no request body.
  $response = $this
    ->request('POST', $url, $request_options);
  $this
    ->assertResourceErrorResponse(400, 'Empty request body.', $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: 403 when unauthorized.
  $response = $this
    ->request('POST', $url, $request_options);
  $reason = $this
    ->getExpectedUnauthorizedAccessMessage('POST');
  $message = trim("The current user is not allowed to POST the selected resource. {$reason}");
  $this
    ->assertResourceErrorResponse(403, $message, $response, '/data');
  $this
    ->setUpAuthorization('POST');
  $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_missing_type;

  // DX: 400 when invalid JSON API request body.
  $response = $this
    ->request('POST', $url, $request_options);
  $this
    ->assertResourceErrorResponse(400, 'Resource object must include a "type".', $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);
  $label_field = $this->entity
    ->getEntityType()
    ->hasKey('label') ? $this->entity
    ->getEntityType()
    ->getKey('label') : static::$labelFieldName;
  $label_field_capitalized = $this->entity
    ->getFieldDefinition($label_field)
    ->getLabel();

  // @todo Remove $expected + assertResourceResponse() in favor of the commented line below once https://www.drupal.org/project/jsonapi/issues/2943176 lands.
  $expected_document = [
    'errors' => [
      [
        'title' => 'Unprocessable Entity',
        'status' => 422,
        'detail' => "{$label_field}: {$label_field_capitalized}: this field cannot hold more than 1 values.",
        'code' => 0,
        'source' => [
          'pointer' => '/data/attributes/' . $label_field,
        ],
      ],
    ],
  ];
  $this
    ->assertResourceResponse(422, $expected_document, $response);

  /* $this->assertResourceErrorResponse(422, "$label_field: $label_field_capitalized: this field cannot hold more than 1 values.", $response, '/data/attributes/' . $label_field); */
  $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_2;

  // DX: 403 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, "IDs should be properly generated and formatted UUIDs as described in RFC 4122.", $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, "The current user is not allowed to POST the selected field (field_rest_test).", $response, '/data/attributes/field_rest_test');
  $request_options[RequestOptions::BODY] = $parseable_invalid_request_body_4;

  // DX: 422 when request document contains non-existent field.
  $response = $this
    ->request('POST', $url, $request_options);
  $this
    ->assertResourceErrorResponse(422, sprintf("The attribute field_nonexistent does not exist on the %s resource type.", static::$resourceTypeName), $response);
  $request_options[RequestOptions::BODY] = $parseable_valid_request_body;

  // @todo Uncomment when https://www.drupal.org/project/jsonapi/issues/2934149 lands.
  // @codingStandardsIgnoreStart

  /*
  $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);
  */

  // @codingStandardsIgnoreEnd
  $request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';

  // 201 for well-formed request.
  $response = $this
    ->request('POST', $url, $request_options);
  $this
    ->assertResourceResponse(201, FALSE, $response);
  $this
    ->assertFalse($response
    ->hasHeader('X-Drupal-Cache'));

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

    // @todo Remove line below in favor of commented line in https://www.drupal.org/project/jsonapi/issues/2878463.
    $location = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), [
      static::$entityTypeId => $uuid,
    ])
      ->setAbsolute(TRUE)
      ->toString();

    /* $location = $this->entityStorage->load(static::$firstCreatedEntityId)->toUrl('jsonapi')->setAbsolute(TRUE)->toString(); */
    $this
      ->assertSame([
      $location,
    ], $response
      ->getHeader('Location'));

    // 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_document = $this
      ->normalize($created_entity, $url);

    // @todo Remove this if-test in https://www.drupal.org/node/2543726: execute
    // its body unconditionally.
    if (static::$entityTypeId !== 'taxonomy_term') {
      $decoded_response_body = Json::decode((string) $response
        ->getBody());
      $this
        ->assertSame($created_entity_document, $decoded_response_body);
    }

    // Assert that the entity was indeed created using the POSTed values.
    foreach ($this
      ->getPostDocument()['data']['attributes'] as $field_name => $field_normalization) {

      // If the value is an array of properties, only verify that the sent
      // properties are present, the server could be computing additional
      // properties.
      if (is_array($field_normalization)) {
        $this
          ->assertArraySubset($field_normalization, $created_entity_document['data']['attributes'][$field_name]);
      }
      else {
        $this
          ->assertSame($field_normalization, $created_entity_document['data']['attributes'][$field_name]);
      }
    }
    if (isset($this
      ->getPostDocument()['data']['relationships'])) {
      foreach ($this
        ->getPostDocument()['data']['relationships'] as $field_name => $relationship_field_normalization) {

        // POSTing relationships: 'data' is required, 'links' is optional.
        static::recursiveKsort($relationship_field_normalization);
        static::recursiveKsort($created_entity_document['data']['relationships'][$field_name]);
        $this
          ->assertSame($relationship_field_normalization, array_diff_key($created_entity_document['data']['relationships'][$field_name], [
          'links' => TRUE,
        ]));
      }
    }
  }
  else {
    $this
      ->assertFalse($response
      ->hasHeader('Location'));
  }

  // 201 for well-formed request that creates another entity.
  // 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);
  $this
    ->assertFalse($response
    ->hasHeader('X-Drupal-Cache'));
  if ($this->entity
    ->getEntityType()
    ->getStorageClass() !== ContentEntityNullStorage::class && $this->entity
    ->getEntityType()
    ->hasKey('uuid')) {
    $uuid = $this->entityStorage
      ->load(static::$secondCreatedEntityId)
      ->uuid();

    // @todo Remove line below in favor of commented line in https://www.drupal.org/project/jsonapi/issues/2878463.
    $location = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), [
      static::$entityTypeId => $uuid,
    ])
      ->setAbsolute(TRUE)
      ->toString();

    /* $location = $this->entityStorage->load(static::$secondCreatedEntityId)->toUrl('jsonapi')->setAbsolute(TRUE)->toString(); */
    $this
      ->assertSame([
      $location,
    ], $response
      ->getHeader('Location'));

    // 500 when creating an entity with a duplicate UUID.
    $doc = $this
      ->getModifiedEntityForPostTesting();
    $doc['data']['id'] = $uuid;
    $doc['data']['attributes'][$label_field] = [
      [
        'value' => $this
          ->randomMachineName(),
      ],
    ];
    $request_options[RequestOptions::BODY] = Json::encode($doc);
    $response = $this
      ->request('POST', $url, $request_options);
    $this
      ->assertResourceErrorResponse(409, 'Conflict: Entity already exists.', $response);

    // 201 when successfully creating an entity with a new UUID.
    $doc = $this
      ->getModifiedEntityForPostTesting();
    $new_uuid = \Drupal::service('uuid')
      ->generate();
    $doc['data']['id'] = $new_uuid;
    $doc['data']['attributes'][$label_field] = [
      [
        'value' => $this
          ->randomMachineName(),
      ],
    ];
    $request_options[RequestOptions::BODY] = Json::encode($doc);
    $response = $this
      ->request('POST', $url, $request_options);
    $this
      ->assertResourceResponse(201, FALSE, $response);
    $entities = $this->entityStorage
      ->loadByProperties([
      'uuid' => $new_uuid,
    ]);
    $new_entity = reset($entities);
    $this
      ->assertNotNull($new_entity);
    $new_entity
      ->delete();
  }
  else {
    $this
      ->assertFalse($response
      ->hasHeader('Location'));
  }
}