public function ResourceTestBase::testPatchIndividual in JSON:API 8
Same name and namespace in other branches
- 8.2 tests/src/Functional/ResourceTestBase.php \Drupal\Tests\jsonapi\Functional\ResourceTestBase::testPatchIndividual()
Tests PATCHing an individual resource, plus edge cases to ensure good DX.
1 call to ResourceTestBase::testPatchIndividual()
- BlockContentTest::testPatchIndividual in tests/
src/ Functional/ BlockContentTest.php - Tests PATCHing an individual resource, plus edge cases to ensure good DX.
4 methods override ResourceTestBase::testPatchIndividual()
- BlockContentTest::testPatchIndividual in tests/
src/ Functional/ BlockContentTest.php - Tests PATCHing an individual resource, plus edge cases to ensure good DX.
- ItemTest::testPatchIndividual in tests/
src/ Functional/ ItemTest.php - Tests PATCHing an individual resource, plus edge cases to ensure good DX.
- MessageTest::testPatchIndividual in tests/
src/ Functional/ MessageTest.php - Tests PATCHing an individual resource, plus edge cases to ensure good DX.
- ShortcutTest::testPatchIndividual in tests/
src/ Functional/ ShortcutTest.php - Tests PATCHing an individual resource, plus edge cases to ensure good DX.
File
- tests/
src/ Functional/ ResourceTestBase.php, line 2021
Class
- ResourceTestBase
- Subclass this for every JSON API resource type.
Namespace
Drupal\Tests\jsonapi\FunctionalCode
public function testPatchIndividual() {
// @todo Remove this in https://www.drupal.org/node/2300677.
if ($this->entity instanceof ConfigEntityInterface) {
$this
->assertTrue(TRUE, 'PATCHing config entities is not yet supported.');
return;
}
// Patch testing requires that another entity of the same type exists.
$this->anotherEntity = $this
->createAnotherEntity('dupe');
// Try with all of the following request bodies.
$unparseable_request_body = '!{>}<';
$parseable_valid_request_body = Json::encode($this
->getPatchDocument());
/* $parseable_valid_request_body_2 = Json::encode($this->getNormalizedPatchEntity()); */
$parseable_invalid_request_body = Json::encode($this
->makeNormalizationInvalid($this
->getPatchDocument(), 'label'));
$parseable_invalid_request_body_2 = Json::encode(NestedArray::mergeDeep([
'data' => [
'attributes' => [
'field_rest_test' => $this
->randomString(),
],
],
], $this
->getPatchDocument()));
// The 'field_rest_test' field does not allow 'view' access, so does not end
// up in the JSON API document. Even when we explicitly add it to the JSON
// API document that we send in a PATCH request, it is considered invalid.
$parseable_invalid_request_body_3 = Json::encode(NestedArray::mergeDeep([
'data' => [
'attributes' => [
'field_rest_test' => $this->entity
->get('field_rest_test')
->getValue(),
],
],
], $this
->getPatchDocument()));
$parseable_invalid_request_body_4 = Json::encode(NestedArray::mergeDeep([
'data' => [
'attributes' => [
'field_nonexistent' => $this
->randomString(),
],
],
], $this
->getPatchDocument()));
// 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.
// @todo Remove line below in favor of commented line in https://www.drupal.org/project/jsonapi/issues/2878463.
$url = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), [
static::$entityTypeId => $this->entity
->uuid(),
]);
/* $url = $this->entity->toUrl('jsonapi'); */
$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.
$response = $this->request('PATCH', $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' => static::$format]);
// DX: 415 when no Content-Type request header.
$response = $this->request('PATCH', $url, $request_options);
$this->assertResourceErrorResponse(415, 'No "Content-Type" request header specified', $response);
$request_options[RequestOptions::HEADERS]['Content-Type'] = static::$mimeType;
*/
// @codingStandardsIgnoreEnd
// DX: 400 when no request body.
$response = $this
->request('PATCH', $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('PATCH', $url, $request_options);
$this
->assertResourceErrorResponse(400, 'Syntax error', $response);
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body;
// DX: 403 when unauthorized.
$response = $this
->request('PATCH', $url, $request_options);
$reason = $this
->getExpectedUnauthorizedAccessMessage('PATCH');
// @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' => 'Forbidden',
'status' => 403,
'detail' => "The current user is not allowed to PATCH the selected resource." . (strlen($reason) ? ' ' . $reason : ''),
'links' => [
'info' => HttpExceptionNormalizer::getInfoUrl(403),
],
'code' => 0,
'id' => '/' . static::$resourceTypeName . '/' . $this->entity
->uuid(),
'source' => [
'pointer' => '/data',
],
],
],
];
$this
->assertResourceResponse(403, $expected_document, $response);
/* $this->assertResourceErrorResponse(403, "The current user is not allowed to PATCH the selected resource." . (strlen($reason) ? ' ' . $reason : ''), $response, '/data'); */
$this
->setUpAuthorization('PATCH');
// DX: 422 when invalid entity: multiple values sent for single-value field.
$response = $this
->request('PATCH', $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 entity contains field without 'edit' access.
$response = $this
->request('PATCH', $url, $request_options);
// @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' => 'Forbidden',
'status' => 403,
'detail' => "The current user is not allowed to PATCH the selected field (field_rest_test).",
'links' => [
'info' => HttpExceptionNormalizer::getInfoUrl(403),
],
'code' => 0,
'id' => '/' . static::$resourceTypeName . '/' . $this->entity
->uuid(),
'source' => [
'pointer' => '/data/attributes/field_rest_test',
],
],
],
];
$this
->assertResourceResponse(403, $expected_document, $response);
/* $this->assertResourceErrorResponse(403, "The current user is not allowed to PATCH the selected field (field_rest_test).", $response, '/data/attributes/field_rest_test'); */
// DX: 403 when entity trying to update an entity's ID field.
$request_options[RequestOptions::BODY] = Json::encode($this
->makeNormalizationInvalid($this
->getPatchDocument(), 'id'));
$response = $this
->request('PATCH', $url, $request_options);
$id_field_name = $this->entity
->getEntityType()
->getKey('id');
// @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' => 'Forbidden',
'status' => 403,
'detail' => "The current user is not allowed to PATCH the selected field ({$id_field_name}). The entity ID cannot be changed.",
'links' => [
'info' => HttpExceptionNormalizer::getInfoUrl(403),
],
'code' => 0,
'id' => '/' . static::$resourceTypeName . '/' . $this->entity
->uuid(),
'source' => [
'pointer' => '/data/attributes/' . $id_field_name,
],
],
],
];
if (floatval(\Drupal::VERSION) < 8.6) {
$expected_document['errors'][0]['detail'] = "The current user is not allowed to PATCH the selected field ({$id_field_name}). The entity ID cannot be changed";
}
$this
->assertResourceResponse(403, $expected_document, $response);
/* $this->assertResourceErrorResponse(403, "The current user is not allowed to PATCH the selected field ($id_field_name). The entity ID cannot be changed", $response, "/data/attributes/$id_field_name"); */
if ($this->entity
->getEntityType()
->hasKey('uuid')) {
// DX: 400 when entity trying to update an entity's UUID field.
$request_options[RequestOptions::BODY] = Json::encode($this
->makeNormalizationInvalid($this
->getPatchDocument(), 'uuid'));
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceErrorResponse(400, sprintf("The selected entity (%s) does not match the ID in the payload (%s).", $this->entity
->uuid(), $this->anotherEntity
->uuid()), $response);
}
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body_3;
// DX: 403 when entity contains field without 'edit' nor 'view' access, even
// when the value for that field matches the current value. This is allowed
// in principle, but leads to information disclosure.
$response = $this
->request('PATCH', $url, $request_options);
// @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' => 'Forbidden',
'status' => 403,
'detail' => "The current user is not allowed to PATCH the selected field (field_rest_test).",
'links' => [
'info' => HttpExceptionNormalizer::getInfoUrl(403),
],
'code' => 0,
'id' => '/' . static::$resourceTypeName . '/' . $this->entity
->uuid(),
'source' => [
'pointer' => '/data/attributes/field_rest_test',
],
],
],
];
$this
->assertResourceResponse(403, $expected_document, $response);
/* $this->assertResourceErrorResponse(403, "The current user is not allowed to PATCH the selected field (field_rest_test).", $response, '/data/attributes/field_rest_test'); */
// DX: 403 when sending PATCH request with updated read-only fields.
list($modified_entity, $original_values) = static::getModifiedEntityForPatchTesting($this->entity);
// Send PATCH request by serializing the modified entity, assert the error
// response, change the modified entity field that caused the error response
// back to its original value, repeat.
foreach (static::$patchProtectedFieldNames as $patch_protected_field_name => $reason) {
$request_options[RequestOptions::BODY] = Json::encode($this
->normalize($modified_entity, $url));
$response = $this
->request('PATCH', $url, $request_options);
// @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' => 'Forbidden',
'status' => 403,
'detail' => "The current user is not allowed to PATCH the selected field (" . $patch_protected_field_name . ")." . ($reason !== NULL ? ' ' . $reason : ''),
'links' => [
'info' => HttpExceptionNormalizer::getInfoUrl(403),
],
'code' => 0,
'id' => '/' . static::$resourceTypeName . '/' . $this->entity
->uuid(),
'source' => [
'pointer' => '/data/attributes/' . $patch_protected_field_name,
],
],
],
];
$this
->assertResourceResponse(403, $expected_document, $response);
/* $this->assertResourceErrorResponse(403, "The current user is not allowed to PATCH the selected field (" . $patch_protected_field_name . ")." . ($reason !== NULL ? ' ' . $reason : ''), $response, '/data/attributes/' . $patch_protected_field_name); */
$modified_entity
->get($patch_protected_field_name)
->setValue($original_values[$patch_protected_field_name]);
}
$request_options[RequestOptions::BODY] = $parseable_invalid_request_body_4;
// DX: 422 when request document contains non-existent field.
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceErrorResponse(422, sprintf("The attribute field_nonexistent does not exist on the %s resource type.", static::$resourceTypeName), $response);
// 200 for well-formed PATCH request that sends all fields (even including
// read-only ones, but with unchanged values).
$valid_request_body = NestedArray::mergeDeep($this
->normalize($this->entity, $url), $this
->getPatchDocument());
$request_options[RequestOptions::BODY] = Json::encode($valid_request_body);
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceResponse(200, FALSE, $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('PATCH', $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';
// 200 for well-formed request.
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceResponse(200, FALSE, $response);
$this
->assertFalse($response
->hasHeader('X-Drupal-Cache'));
// Assert that the entity was indeed updated, and that the response body
// contains the serialized updated entity.
$updated_entity = $this->entityStorage
->loadUnchanged($this->entity
->id());
$updated_entity_document = $this
->normalize($updated_entity, $url);
$this
->assertSame($updated_entity_document, Json::decode((string) $response
->getBody()));
// Assert that the entity was indeed created using the PATCHed values.
foreach ($this
->getPatchDocument() as $field_name => $field_normalization) {
// Some top-level keys in the normalization may not be fields on the
// entity (for example '_links' and '_embedded' in the HAL normalization).
if ($updated_entity
->hasField($field_name)) {
// Subset, not same, because we can e.g. send just the target_id for the
// bundle in a PATCH request; the response will include more properties.
$this
->assertArraySubset($field_normalization, $updated_entity
->get($field_name)
->getValue(), TRUE);
}
}
// Ensure that fields do not get deleted if they're not present in the PATCH
// request. Test this using the configurable field that we added, but which
// is not sent in the PATCH request.
$this
->assertSame('All the faith he had had had had no effect on the outcome of his life.', $updated_entity
->get('field_rest_test')->value);
// @todo Remove this when JSON API requires Drupal 8.5 or newer.
if (floatval(\Drupal::VERSION) < 8.5) {
return;
}
// Multi-value field: remove item 0. Then item 1 becomes item 0.
$doc_multi_value_tests = $this
->getPatchDocument();
$doc_multi_value_tests['data']['attributes']['field_rest_test_multivalue'] = $this->entity
->get('field_rest_test_multivalue')
->getValue();
$doc_remove_item = $doc_multi_value_tests;
unset($doc_remove_item['data']['attributes']['field_rest_test_multivalue'][0]);
$request_options[RequestOptions::BODY] = Json::encode($doc_remove_item, 'api_json');
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceResponse(200, FALSE, $response);
$this
->assertSame([
0 => [
'value' => 'Two',
],
], $this->entityStorage
->loadUnchanged($this->entity
->id())
->get('field_rest_test_multivalue')
->getValue());
// Multi-value field: add one item before the existing one, and one after.
$doc_add_items = $doc_multi_value_tests;
$doc_add_items['data']['attributes']['field_rest_test_multivalue'][2] = [
'value' => 'Three',
];
$request_options[RequestOptions::BODY] = Json::encode($doc_add_items);
$response = $this
->request('PATCH', $url, $request_options);
$this
->assertResourceResponse(200, FALSE, $response);
$expected_document = [
0 => [
'value' => 'One',
],
1 => [
'value' => 'Two',
],
2 => [
'value' => 'Three',
],
];
$this
->assertSame($expected_document, $this->entityStorage
->loadUnchanged($this->entity
->id())
->get('field_rest_test_multivalue')
->getValue());
}