You are here

public function JsonApiFunctionalTest::testWrite in Drupal 9

Same name and namespace in other branches
  1. 8 core/modules/jsonapi/tests/src/Functional/JsonApiFunctionalTest.php \Drupal\Tests\jsonapi\Functional\JsonApiFunctionalTest::testWrite()

Tests POST, PATCH and DELETE.

File

core/modules/jsonapi/tests/src/Functional/JsonApiFunctionalTest.php, line 535

Class

JsonApiFunctionalTest
General functional test class.

Namespace

Drupal\Tests\jsonapi\Functional

Code

public function testWrite() {
  $this
    ->config('jsonapi.settings')
    ->set('read_only', FALSE)
    ->save(TRUE);
  $this
    ->createDefaultContent(0, 3, FALSE, FALSE, static::IS_NOT_MULTILINGUAL, FALSE);

  // 1. Successful post.
  $collection_url = Url::fromRoute('jsonapi.node--article.collection.post');
  $body = [
    'data' => [
      'type' => 'node--article',
      'attributes' => [
        'langcode' => 'en',
        'title' => 'My custom title',
        'default_langcode' => '1',
        'body' => [
          'value' => 'Custom value',
          'format' => 'plain_text',
          'summary' => 'Custom summary',
        ],
      ],
      'relationships' => [
        'field_tags' => [
          'data' => [
            [
              'type' => 'taxonomy_term--tags',
              'id' => $this->tags[0]
                ->uuid(),
            ],
            [
              'type' => 'taxonomy_term--tags',
              'id' => $this->tags[1]
                ->uuid(),
            ],
          ],
        ],
      ],
    ],
  ];
  $response = $this
    ->request('POST', $collection_url, [
    'body' => Json::encode($body),
    'auth' => [
      $this->user
        ->getAccountName(),
      $this->user->pass_raw,
    ],
    'headers' => [
      'Content-Type' => 'application/vnd.api+json',
    ],
  ]);
  $created_response = Json::decode($response
    ->getBody()
    ->__toString());
  $this
    ->assertEquals(201, $response
    ->getStatusCode());
  $this
    ->assertArrayNotHasKey('uuid', $created_response['data']['attributes']);
  $uuid = $created_response['data']['id'];
  $this
    ->assertCount(2, $created_response['data']['relationships']['field_tags']['data']);
  $this
    ->assertEquals($created_response['data']['links']['self']['href'], $response
    ->getHeader('Location')[0]);

  // 2. Authorization error.
  $response = $this
    ->request('POST', $collection_url, [
    'body' => Json::encode($body),
    'headers' => [
      'Content-Type' => 'application/vnd.api+json',
    ],
  ]);
  $created_response = Json::decode($response
    ->getBody()
    ->__toString());
  $this
    ->assertEquals(401, $response
    ->getStatusCode());
  $this
    ->assertNotEmpty($created_response['errors']);
  $this
    ->assertEquals('Unauthorized', $created_response['errors'][0]['title']);

  // 2.1 Authorization error with a user without create permissions.
  $response = $this
    ->request('POST', $collection_url, [
    'body' => Json::encode($body),
    'auth' => [
      $this->userCanViewProfiles
        ->getAccountName(),
      $this->userCanViewProfiles->pass_raw,
    ],
    'headers' => [
      'Content-Type' => 'application/vnd.api+json',
    ],
  ]);
  $created_response = Json::decode($response
    ->getBody()
    ->__toString());
  $this
    ->assertEquals(403, $response
    ->getStatusCode());
  $this
    ->assertNotEmpty($created_response['errors']);
  $this
    ->assertEquals('Forbidden', $created_response['errors'][0]['title']);

  // 3. Missing Content-Type error.
  $response = $this
    ->request('POST', $collection_url, [
    'body' => Json::encode($body),
    'auth' => [
      $this->user
        ->getAccountName(),
      $this->user->pass_raw,
    ],
    'headers' => [
      'Accept' => 'application/vnd.api+json',
    ],
  ]);
  $created_response = Json::decode($response
    ->getBody()
    ->__toString());
  $this
    ->assertEquals(415, $response
    ->getStatusCode());

  // 4. Article with a duplicate ID.
  $invalid_body = $body;
  $invalid_body['data']['id'] = Node::load(1)
    ->uuid();
  $response = $this
    ->request('POST', $collection_url, [
    'body' => Json::encode($invalid_body),
    'auth' => [
      $this->user
        ->getAccountName(),
      $this->user->pass_raw,
    ],
    'headers' => [
      'Accept' => 'application/vnd.api+json',
      'Content-Type' => 'application/vnd.api+json',
    ],
  ]);
  $created_response = Json::decode($response
    ->getBody()
    ->__toString());
  $this
    ->assertEquals(409, $response
    ->getStatusCode());
  $this
    ->assertNotEmpty($created_response['errors']);
  $this
    ->assertEquals('Conflict', $created_response['errors'][0]['title']);

  // 5. Article with wrong reference UUIDs for tags.
  $body_invalid_tags = $body;
  $body_invalid_tags['data']['relationships']['field_tags']['data'][0]['id'] = 'lorem';
  $body_invalid_tags['data']['relationships']['field_tags']['data'][1]['id'] = 'ipsum';
  $response = $this
    ->request('POST', $collection_url, [
    'body' => Json::encode($body_invalid_tags),
    'auth' => [
      $this->user
        ->getAccountName(),
      $this->user->pass_raw,
    ],
    'headers' => [
      'Content-Type' => 'application/vnd.api+json',
    ],
  ]);
  $created_response = Json::decode($response
    ->getBody()
    ->__toString());
  $this
    ->assertEquals(404, $response
    ->getStatusCode());

  // 6. Decoding error.
  $response = $this
    ->request('POST', $collection_url, [
    'body' => '{"bad json",,,}',
    'auth' => [
      $this->user
        ->getAccountName(),
      $this->user->pass_raw,
    ],
    'headers' => [
      'Content-Type' => 'application/vnd.api+json',
      'Accept' => 'application/vnd.api+json',
    ],
  ]);
  $created_response = Json::decode($response
    ->getBody()
    ->__toString());
  $this
    ->assertEquals(400, $response
    ->getStatusCode());
  $this
    ->assertNotEmpty($created_response['errors']);
  $this
    ->assertEquals('Bad Request', $created_response['errors'][0]['title']);

  // 6.1 Denormalizing error.
  $response = $this
    ->request('POST', $collection_url, [
    'body' => '{"data":{"type":"something"},"valid yet nonsensical json":[]}',
    'auth' => [
      $this->user
        ->getAccountName(),
      $this->user->pass_raw,
    ],
    'headers' => [
      'Content-Type' => 'application/vnd.api+json',
      'Accept' => 'application/vnd.api+json',
    ],
  ]);
  $created_response = Json::decode($response
    ->getBody()
    ->__toString());
  $this
    ->assertEquals(422, $response
    ->getStatusCode());
  $this
    ->assertNotEmpty($created_response['errors']);
  $this
    ->assertEquals('Unprocessable Entity', $created_response['errors'][0]['title']);

  // 6.2 Relationships are not included in "data".
  $malformed_body = $body;
  unset($malformed_body['data']['relationships']);
  $malformed_body['relationships'] = $body['data']['relationships'];
  $response = $this
    ->request('POST', $collection_url, [
    'body' => Json::encode($malformed_body),
    'auth' => [
      $this->user
        ->getAccountName(),
      $this->user->pass_raw,
    ],
    'headers' => [
      'Accept' => 'application/vnd.api+json',
      'Content-Type' => 'application/vnd.api+json',
    ],
  ]);
  $created_response = Json::decode((string) $response
    ->getBody());
  $this
    ->assertSame(400, $response
    ->getStatusCode());
  $this
    ->assertNotEmpty($created_response['errors']);
  $this
    ->assertSame("Bad Request", $created_response['errors'][0]['title']);
  $this
    ->assertSame("Found \"relationships\" within the document's top level. The \"relationships\" key must be within resource object.", $created_response['errors'][0]['detail']);

  // 6.2 "type" not included in "data".
  $missing_type = $body;
  unset($missing_type['data']['type']);
  $response = $this
    ->request('POST', $collection_url, [
    'body' => Json::encode($missing_type),
    'auth' => [
      $this->user
        ->getAccountName(),
      $this->user->pass_raw,
    ],
    'headers' => [
      'Accept' => 'application/vnd.api+json',
      'Content-Type' => 'application/vnd.api+json',
    ],
  ]);
  $created_response = Json::decode((string) $response
    ->getBody());
  $this
    ->assertSame(400, $response
    ->getStatusCode());
  $this
    ->assertNotEmpty($created_response['errors']);
  $this
    ->assertSame("Bad Request", $created_response['errors'][0]['title']);
  $this
    ->assertSame("Resource object must include a \"type\".", $created_response['errors'][0]['detail']);

  // 7. Successful PATCH.
  $body = [
    'data' => [
      'id' => $uuid,
      'type' => 'node--article',
      'attributes' => [
        'title' => 'My updated title',
      ],
    ],
  ];
  $individual_url = Url::fromRoute('jsonapi.node--article.individual', [
    'entity' => $uuid,
  ]);
  $response = $this
    ->request('PATCH', $individual_url, [
    'body' => Json::encode($body),
    'auth' => [
      $this->user
        ->getAccountName(),
      $this->user->pass_raw,
    ],
    'headers' => [
      'Content-Type' => 'application/vnd.api+json',
    ],
  ]);
  $updated_response = Json::decode($response
    ->getBody()
    ->__toString());
  $this
    ->assertEquals(200, $response
    ->getStatusCode());
  $this
    ->assertEquals('My updated title', $updated_response['data']['attributes']['title']);

  // 7.1 Unsuccessful PATCH due to access restrictions.
  $body = [
    'data' => [
      'id' => $uuid,
      'type' => 'node--article',
      'attributes' => [
        'title' => 'My updated title',
      ],
    ],
  ];
  $individual_url = Url::fromRoute('jsonapi.node--article.individual', [
    'entity' => $uuid,
  ]);
  $response = $this
    ->request('PATCH', $individual_url, [
    'body' => Json::encode($body),
    'auth' => [
      $this->userCanViewProfiles
        ->getAccountName(),
      $this->userCanViewProfiles->pass_raw,
    ],
    'headers' => [
      'Content-Type' => 'application/vnd.api+json',
    ],
  ]);
  $this
    ->assertEquals(403, $response
    ->getStatusCode());

  // 8. Field access forbidden check.
  $body = [
    'data' => [
      'id' => $uuid,
      'type' => 'node--article',
      'attributes' => [
        'title' => 'My updated title',
        'status' => 0,
      ],
    ],
  ];
  $response = $this
    ->request('PATCH', $individual_url, [
    'body' => Json::encode($body),
    'auth' => [
      $this->user
        ->getAccountName(),
      $this->user->pass_raw,
    ],
    'headers' => [
      'Content-Type' => 'application/vnd.api+json',
    ],
  ]);
  $updated_response = Json::decode($response
    ->getBody()
    ->__toString());
  $this
    ->assertEquals(403, $response
    ->getStatusCode());
  $this
    ->assertEquals("The current user is not allowed to PATCH the selected field (status). The 'administer nodes' permission is required.", $updated_response['errors'][0]['detail']);
  $node = \Drupal::service('entity.repository')
    ->loadEntityByUuid('node', $uuid);
  $this
    ->assertEquals(1, $node
    ->get('status')->value, 'Node status was not changed.');

  // 9. Successful POST to related endpoint.
  $body = [
    'data' => [
      [
        'id' => $this->tags[2]
          ->uuid(),
        'type' => 'taxonomy_term--tags',
      ],
    ],
  ];
  $relationship_url = Url::fromRoute('jsonapi.node--article.field_tags.relationship.post', [
    'entity' => $uuid,
  ]);
  $response = $this
    ->request('POST', $relationship_url, [
    'body' => Json::encode($body),
    'auth' => [
      $this->user
        ->getAccountName(),
      $this->user->pass_raw,
    ],
    'headers' => [
      'Content-Type' => 'application/vnd.api+json',
    ],
  ]);
  $updated_response = Json::decode($response
    ->getBody()
    ->__toString());
  $this
    ->assertEquals(200, $response
    ->getStatusCode());
  $this
    ->assertCount(3, $updated_response['data']);
  $this
    ->assertEquals('taxonomy_term--tags', $updated_response['data'][2]['type']);
  $this
    ->assertEquals($this->tags[2]
    ->uuid(), $updated_response['data'][2]['id']);

  // 10. Successful PATCH to related endpoint.
  $body = [
    'data' => [
      [
        'id' => $this->tags[1]
          ->uuid(),
        'type' => 'taxonomy_term--tags',
      ],
    ],
  ];
  $response = $this
    ->request('PATCH', $relationship_url, [
    'body' => Json::encode($body),
    'auth' => [
      $this->user
        ->getAccountName(),
      $this->user->pass_raw,
    ],
    'headers' => [
      'Content-Type' => 'application/vnd.api+json',
    ],
  ]);
  $this
    ->assertEquals(204, $response
    ->getStatusCode());
  $this
    ->assertEmpty($response
    ->getBody()
    ->__toString());

  // 11. Successful DELETE to related endpoint.
  $response = $this
    ->request('DELETE', $relationship_url, [
    // Send a request with no body.
    'auth' => [
      $this->user
        ->getAccountName(),
      $this->user->pass_raw,
    ],
    'headers' => [
      'Content-Type' => 'application/vnd.api+json',
      'Accept' => 'application/vnd.api+json',
    ],
  ]);
  $updated_response = Json::decode($response
    ->getBody()
    ->__toString());
  $this
    ->assertEquals('You need to provide a body for DELETE operations on a relationship (field_tags).', $updated_response['errors'][0]['detail']);
  $this
    ->assertEquals(400, $response
    ->getStatusCode());
  $response = $this
    ->request('DELETE', $relationship_url, [
    // Send a request with no authentication.
    'body' => Json::encode($body),
    'headers' => [
      'Content-Type' => 'application/vnd.api+json',
    ],
  ]);
  $this
    ->assertEquals(401, $response
    ->getStatusCode());
  $response = $this
    ->request('DELETE', $relationship_url, [
    // Remove the existing relationship item.
    'body' => Json::encode($body),
    'auth' => [
      $this->user
        ->getAccountName(),
      $this->user->pass_raw,
    ],
    'headers' => [
      'Content-Type' => 'application/vnd.api+json',
    ],
  ]);
  $this
    ->assertEquals(204, $response
    ->getStatusCode());
  $this
    ->assertEmpty($response
    ->getBody()
    ->__toString());

  // 12. PATCH with invalid title and body format.
  $body = [
    'data' => [
      'id' => $uuid,
      'type' => 'node--article',
      'attributes' => [
        'title' => '',
        'body' => [
          'value' => 'Custom value',
          'format' => 'invalid_format',
          'summary' => 'Custom summary',
        ],
      ],
    ],
  ];
  $response = $this
    ->request('PATCH', $individual_url, [
    'body' => Json::encode($body),
    'auth' => [
      $this->user
        ->getAccountName(),
      $this->user->pass_raw,
    ],
    'headers' => [
      'Content-Type' => 'application/vnd.api+json',
      'Accept' => 'application/vnd.api+json',
    ],
  ]);
  $updated_response = Json::decode($response
    ->getBody()
    ->__toString());
  $this
    ->assertEquals(422, $response
    ->getStatusCode());
  $this
    ->assertCount(2, $updated_response['errors']);
  for ($i = 0; $i < 2; $i++) {
    $this
      ->assertEquals("Unprocessable Entity", $updated_response['errors'][$i]['title']);
    $this
      ->assertEquals(422, $updated_response['errors'][$i]['status']);
  }
  $this
    ->assertEquals("title: This value should not be null.", $updated_response['errors'][0]['detail']);
  $this
    ->assertEquals("body.0.format: The value you selected is not a valid choice.", $updated_response['errors'][1]['detail']);
  $this
    ->assertEquals("/data/attributes/title", $updated_response['errors'][0]['source']['pointer']);
  $this
    ->assertEquals("/data/attributes/body/format", $updated_response['errors'][1]['source']['pointer']);

  // 13. PATCH with field that doesn't exist on Entity.
  $body = [
    'data' => [
      'id' => $uuid,
      'type' => 'node--article',
      'attributes' => [
        'field_that_does_not_exist' => 'foobar',
      ],
    ],
  ];
  $response = $this
    ->request('PATCH', $individual_url, [
    'body' => Json::encode($body),
    'auth' => [
      $this->user
        ->getAccountName(),
      $this->user->pass_raw,
    ],
    'headers' => [
      'Content-Type' => 'application/vnd.api+json',
      'Accept' => 'application/vnd.api+json',
    ],
  ]);
  $updated_response = Json::decode($response
    ->getBody()
    ->__toString());
  $this
    ->assertEquals(422, $response
    ->getStatusCode());
  $this
    ->assertEquals("The attribute field_that_does_not_exist does not exist on the node--article resource type.", $updated_response['errors']['0']['detail']);

  // 14. Successful DELETE.
  $response = $this
    ->request('DELETE', $individual_url, [
    'auth' => [
      $this->user
        ->getAccountName(),
      $this->user->pass_raw,
    ],
  ]);
  $this
    ->assertEquals(204, $response
    ->getStatusCode());
  $response = $this
    ->request('GET', $individual_url, []);
  $this
    ->assertEquals(404, $response
    ->getStatusCode());
}