function ResourceTestBase::doTestPostIndividual

Same name in other branches
  1. 11.x core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php \Drupal\Tests\jsonapi\Functional\ResourceTestBase::doTestPostIndividual()

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

1 call to ResourceTestBase::doTestPostIndividual()
ResourceTestBase::testIndividual in core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php
Tests POST/PATCH/DELETE for an individual resource.
1 method overrides ResourceTestBase::doTestPostIndividual()
MediaTest::doTestPostIndividual in core/modules/jsonapi/tests/src/Functional/MediaTest.php
Tests POSTing an individual resource, plus edge cases to ensure good DX.

File

core/modules/jsonapi/tests/src/Functional/ResourceTestBase.php, line 1983

Class

ResourceTestBase
Subclass this for every JSON:API resource type.

Namespace

Drupal\Tests\jsonapi\Functional

Code

protected function doTestPostIndividual() : void {
    // @todo Remove this in https://www.drupal.org/node/2300677.
    if ($this->entity instanceof ConfigEntityInterface) {
        $this->markTestSkipped('POSTing config entities is not yet supported.');
    }
    // Try with all of the following request bodies.
    $not_parseable_request_body = '!{>}<';
    $parseable_valid_request_body = Json::encode($this->getPostDocument());
    $parseable_invalid_request_body_missing_type = Json::encode($this->removeResourceTypeFromDocument($this->getPostDocument()));
    if ($this->entity
        ->getEntityType()
        ->hasKey('label')) {
        $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.post', static::$resourceTypeName));
    $request_options = [];
    $request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
    $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
    // DX: 405 when read-only mode is enabled.
    $response = $this->request('POST', $url, $request_options);
    $this->assertResourceErrorResponse(405, sprintf("JSON:API is configured to accept only read operations. Site administrators can configure this at %s.", Url::fromUri('base:/admin/config/services/jsonapi')->setAbsolute()
        ->toString(TRUE)
        ->getGeneratedUrl()), $url, $response);
    if ($this->resourceType
        ->isLocatable()) {
        $this->assertSame([
            'GET',
        ], $response->getHeader('Allow'));
    }
    else {
        $this->assertSame([
            '',
        ], $response->getHeader('Allow'));
    }
    $this->config('jsonapi.settings')
        ->set('read_only', FALSE)
        ->save(TRUE);
    // DX: 415 when no Content-Type request header.
    $response = $this->request('POST', $url, $request_options);
    $this->assertSame(415, $response->getStatusCode());
    $request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';
    // DX: 403 when unauthorized.
    $response = $this->request('POST', $url, $request_options);
    $reason = $this->getExpectedUnauthorizedAccessMessage('POST');
    $this->assertResourceErrorResponse(403, (string) $reason, $url, $response);
    $this->setUpAuthorization('POST');
    // DX: 400 when no request body.
    $response = $this->request('POST', $url, $request_options);
    $this->assertResourceErrorResponse(400, 'Empty request body.', $url, $response, FALSE);
    $request_options[RequestOptions::BODY] = $not_parseable_request_body;
    // DX: 400 when un-parseable request body.
    $response = $this->request('POST', $url, $request_options);
    $this->assertResourceErrorResponse(400, 'Syntax error', $url, $response, FALSE);
    $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".', $url, $response, FALSE);
    if ($this->entity
        ->getEntityType()
        ->hasKey('label')) {
        $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()
            ->getKey('label');
        $label_field_capitalized = $this->entity
            ->getFieldDefinition($label_field)
            ->getLabel();
        $this->assertResourceErrorResponse(422, "{$label_field}: {$label_field_capitalized}: this field cannot hold more than 1 values.", NULL, $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/project/drupal/issues/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.", $url, $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).", $url, $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), $url, $response, FALSE);
    $request_options[RequestOptions::BODY] = $parseable_valid_request_body;
    $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"', $url, $response);
    $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) {
        $created_entity = $this->entityLoadUnchanged(static::$firstCreatedEntityId);
        $uuid = $created_entity->uuid();
        // @todo Remove line below in favor of commented line in https://www.drupal.org/project/drupal/issues/2878463.
        $location = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), [
            'entity' => $uuid,
        ]);
        if (static::$resourceTypeIsVersionable) {
            assert($created_entity instanceof RevisionableInterface);
            $location->setOption('query', [
                'resourceVersion' => 'id:' . $created_entity->getRevisionId(),
            ]);
        }
        
        /* $location = $this->entityStorage->load(static::$firstCreatedEntityId)->toUrl('jsonapi')->setAbsolute(TRUE)->toString(); */
        $this->assertSame([
            $location->setAbsolute()
                ->toString(),
        ], $response->getHeader('Location'));
        // Assert that the entity was indeed created, and that the response body
        // contains the serialized created entity.
        $created_entity_document = $this->normalize($created_entity, $url);
        $decoded_response_body = $this->getDocumentFromResponse($response);
        $this->assertEquals($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)) {
                foreach ($field_normalization as $value) {
                    $this->assertContains($value, $created_entity_document['data']['attributes'][$field_name]);
                }
            }
            else {
                $this->assertEquals($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->assertEquals($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')) {
        $second_created_entity = $this->entityStorage
            ->load(static::$secondCreatedEntityId);
        $uuid = $second_created_entity->uuid();
        // @todo Remove line below in favor of commented line in https://www.drupal.org/project/drupal/issues/2878463.
        $location = Url::fromRoute(sprintf('jsonapi.%s.individual', static::$resourceTypeName), [
            'entity' => $uuid,
        ]);
        
        /* $location = $this->entityStorage->load(static::$secondCreatedEntityId)->toUrl('jsonapi')->setAbsolute(TRUE)->toString(); */
        if (static::$resourceTypeIsVersionable) {
            assert($created_entity instanceof RevisionableInterface);
            $location->setOption('query', [
                'resourceVersion' => 'id:' . $second_created_entity->getRevisionId(),
            ]);
        }
        $this->assertSame([
            $location->setAbsolute()
                ->toString(),
        ], $response->getHeader('Location'));
        // 500 when creating an entity with a duplicate UUID.
        $doc = $this->getModifiedEntityForPostTesting();
        $doc['data']['id'] = $uuid;
        $label_field = $this->entity
            ->getEntityType()
            ->hasKey('label') ? $this->entity
            ->getEntityType()
            ->getKey('label') : static::$labelFieldName;
        if (isset($label_field)) {
            $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.', $url, $response, FALSE);
        // 201 when successfully creating an entity with a new UUID.
        $doc = $this->getModifiedEntityForPostTesting();
        $new_uuid = \Drupal::service('uuid')->generate();
        $doc['data']['id'] = $new_uuid;
        if (isset($label_field)) {
            $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([
            $this->uuidKey => $new_uuid,
        ]);
        $new_entity = reset($entities);
        $this->assertNotNull($new_entity);
        $new_entity->delete();
    }
    else {
        $this->assertFalse($response->hasHeader('Location'));
    }
}

Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.