annotate core/modules/jsonapi/tests/src/Functional/CommentTest.php @ 19:fa3358dc1485 tip

Add ndrum files
author Chris Cannam
date Wed, 28 Aug 2019 13:14:47 +0100
parents af1871eacc83
children
rev   line source
Chris@18 1 <?php
Chris@18 2
Chris@18 3 namespace Drupal\Tests\jsonapi\Functional;
Chris@18 4
Chris@18 5 use Drupal\comment\Entity\Comment;
Chris@18 6 use Drupal\comment\Entity\CommentType;
Chris@18 7 use Drupal\comment\Tests\CommentTestTrait;
Chris@18 8 use Drupal\Component\Serialization\Json;
Chris@18 9 use Drupal\Component\Utility\NestedArray;
Chris@18 10 use Drupal\Core\Cache\Cache;
Chris@18 11 use Drupal\Core\Entity\EntityInterface;
Chris@18 12 use Drupal\Core\Session\AccountInterface;
Chris@18 13 use Drupal\Core\Url;
Chris@18 14 use Drupal\entity_test\Entity\EntityTest;
Chris@18 15 use Drupal\Tests\jsonapi\Traits\CommonCollectionFilterAccessTestPatternsTrait;
Chris@18 16 use Drupal\user\Entity\User;
Chris@18 17 use GuzzleHttp\RequestOptions;
Chris@18 18
Chris@18 19 /**
Chris@18 20 * JSON:API integration test for the "Comment" content entity type.
Chris@18 21 *
Chris@18 22 * @group jsonapi
Chris@18 23 */
Chris@18 24 class CommentTest extends ResourceTestBase {
Chris@18 25
Chris@18 26 use CommentTestTrait;
Chris@18 27 use CommonCollectionFilterAccessTestPatternsTrait;
Chris@18 28
Chris@18 29 /**
Chris@18 30 * {@inheritdoc}
Chris@18 31 */
Chris@18 32 public static $modules = ['comment', 'entity_test'];
Chris@18 33
Chris@18 34 /**
Chris@18 35 * {@inheritdoc}
Chris@18 36 */
Chris@18 37 protected static $entityTypeId = 'comment';
Chris@18 38
Chris@18 39 /**
Chris@18 40 * {@inheritdoc}
Chris@18 41 */
Chris@18 42 protected static $resourceTypeName = 'comment--comment';
Chris@18 43
Chris@18 44 /**
Chris@18 45 * {@inheritdoc}
Chris@18 46 */
Chris@18 47 protected static $patchProtectedFieldNames = [
Chris@18 48 'status' => "The 'administer comments' permission is required.",
Chris@18 49 'name' => "The 'administer comments' permission is required.",
Chris@18 50 'homepage' => "The 'administer comments' permission is required.",
Chris@18 51 'created' => "The 'administer comments' permission is required.",
Chris@18 52 'changed' => NULL,
Chris@18 53 'thread' => NULL,
Chris@18 54 'entity_type' => NULL,
Chris@18 55 'field_name' => NULL,
Chris@18 56 // @todo Uncomment this after https://www.drupal.org/project/drupal/issues/1847608 lands. Until then, it's impossible to test this.
Chris@18 57 // 'pid' => NULL,
Chris@18 58 'uid' => "The 'administer comments' permission is required.",
Chris@18 59 'entity_id' => NULL,
Chris@18 60 ];
Chris@18 61
Chris@18 62 /**
Chris@18 63 * {@inheritdoc}
Chris@18 64 *
Chris@18 65 * @var \Drupal\comment\CommentInterface
Chris@18 66 */
Chris@18 67 protected $entity;
Chris@18 68
Chris@18 69 /**
Chris@18 70 * {@inheritdoc}
Chris@18 71 */
Chris@18 72 protected function setUpAuthorization($method) {
Chris@18 73 switch ($method) {
Chris@18 74 case 'GET':
Chris@18 75 $this->grantPermissionsToTestedRole(['access comments', 'view test entity']);
Chris@18 76 break;
Chris@18 77
Chris@18 78 case 'POST':
Chris@18 79 $this->grantPermissionsToTestedRole(['post comments']);
Chris@18 80 break;
Chris@18 81
Chris@18 82 case 'PATCH':
Chris@18 83 $this->grantPermissionsToTestedRole(['edit own comments']);
Chris@18 84 break;
Chris@18 85
Chris@18 86 case 'DELETE':
Chris@18 87 $this->grantPermissionsToTestedRole(['administer comments']);
Chris@18 88 break;
Chris@18 89 }
Chris@18 90 }
Chris@18 91
Chris@18 92 /**
Chris@18 93 * {@inheritdoc}
Chris@18 94 */
Chris@18 95 protected function createEntity() {
Chris@18 96 // Create a "bar" bundle for the "entity_test" entity type and create.
Chris@18 97 $bundle = 'bar';
Chris@18 98 entity_test_create_bundle($bundle, NULL, 'entity_test');
Chris@18 99
Chris@18 100 // Create a comment field on this bundle.
Chris@18 101 $this->addDefaultCommentField('entity_test', 'bar', 'comment');
Chris@18 102
Chris@18 103 // Create a "Camelids" test entity that the comment will be assigned to.
Chris@18 104 $commented_entity = EntityTest::create([
Chris@18 105 'name' => 'Camelids',
Chris@18 106 'type' => 'bar',
Chris@18 107 ]);
Chris@18 108 $commented_entity->save();
Chris@18 109
Chris@18 110 // Create a "Llama" comment.
Chris@18 111 $comment = Comment::create([
Chris@18 112 'comment_body' => [
Chris@18 113 'value' => 'The name "llama" was adopted by European settlers from native Peruvians.',
Chris@18 114 'format' => 'plain_text',
Chris@18 115 ],
Chris@18 116 'entity_id' => $commented_entity->id(),
Chris@18 117 'entity_type' => 'entity_test',
Chris@18 118 'field_name' => 'comment',
Chris@18 119 ]);
Chris@18 120 $comment->setSubject('Llama')
Chris@18 121 ->setOwnerId($this->account->id())
Chris@18 122 ->setPublished()
Chris@18 123 ->setCreatedTime(123456789)
Chris@18 124 ->setChangedTime(123456789);
Chris@18 125 $comment->save();
Chris@18 126
Chris@18 127 return $comment;
Chris@18 128 }
Chris@18 129
Chris@18 130 /**
Chris@18 131 * {@inheritdoc}
Chris@18 132 */
Chris@18 133 protected function getExpectedDocument() {
Chris@18 134 $self_url = Url::fromUri('base:/jsonapi/comment/comment/' . $this->entity->uuid())->setAbsolute()->toString(TRUE)->getGeneratedUrl();
Chris@18 135 $author = User::load($this->entity->getOwnerId());
Chris@18 136 return [
Chris@18 137 'jsonapi' => [
Chris@18 138 'meta' => [
Chris@18 139 'links' => [
Chris@18 140 'self' => ['href' => 'http://jsonapi.org/format/1.0/'],
Chris@18 141 ],
Chris@18 142 ],
Chris@18 143 'version' => '1.0',
Chris@18 144 ],
Chris@18 145 'links' => [
Chris@18 146 'self' => ['href' => $self_url],
Chris@18 147 ],
Chris@18 148 'data' => [
Chris@18 149 'id' => $this->entity->uuid(),
Chris@18 150 'type' => 'comment--comment',
Chris@18 151 'links' => [
Chris@18 152 'self' => ['href' => $self_url],
Chris@18 153 ],
Chris@18 154 'attributes' => [
Chris@18 155 'created' => '1973-11-29T21:33:09+00:00',
Chris@18 156 'changed' => (new \DateTime())->setTimestamp($this->entity->getChangedTime())->setTimezone(new \DateTimeZone('UTC'))->format(\DateTime::RFC3339),
Chris@18 157 'comment_body' => [
Chris@18 158 'value' => 'The name "llama" was adopted by European settlers from native Peruvians.',
Chris@18 159 'format' => 'plain_text',
Chris@18 160 'processed' => "<p>The name &quot;llama&quot; was adopted by European settlers from native Peruvians.</p>\n",
Chris@18 161 ],
Chris@18 162 'default_langcode' => TRUE,
Chris@18 163 'entity_type' => 'entity_test',
Chris@18 164 'field_name' => 'comment',
Chris@18 165 'homepage' => NULL,
Chris@18 166 'langcode' => 'en',
Chris@18 167 'name' => NULL,
Chris@18 168 'status' => TRUE,
Chris@18 169 'subject' => 'Llama',
Chris@18 170 'thread' => '01/',
Chris@18 171 'drupal_internal__cid' => 1,
Chris@18 172 ],
Chris@18 173 'relationships' => [
Chris@18 174 'uid' => [
Chris@18 175 'data' => [
Chris@18 176 'id' => $author->uuid(),
Chris@18 177 'type' => 'user--user',
Chris@18 178 ],
Chris@18 179 'links' => [
Chris@18 180 'related' => ['href' => $self_url . '/uid'],
Chris@18 181 'self' => ['href' => $self_url . '/relationships/uid'],
Chris@18 182 ],
Chris@18 183 ],
Chris@18 184 'comment_type' => [
Chris@18 185 'data' => [
Chris@18 186 'id' => CommentType::load('comment')->uuid(),
Chris@18 187 'type' => 'comment_type--comment_type',
Chris@18 188 ],
Chris@18 189 'links' => [
Chris@18 190 'related' => ['href' => $self_url . '/comment_type'],
Chris@18 191 'self' => ['href' => $self_url . '/relationships/comment_type'],
Chris@18 192 ],
Chris@18 193 ],
Chris@18 194 'entity_id' => [
Chris@18 195 'data' => [
Chris@18 196 'id' => EntityTest::load(1)->uuid(),
Chris@18 197 'type' => 'entity_test--bar',
Chris@18 198 ],
Chris@18 199 'links' => [
Chris@18 200 'related' => ['href' => $self_url . '/entity_id'],
Chris@18 201 'self' => ['href' => $self_url . '/relationships/entity_id'],
Chris@18 202 ],
Chris@18 203 ],
Chris@18 204 'pid' => [
Chris@18 205 'data' => NULL,
Chris@18 206 'links' => [
Chris@18 207 'related' => ['href' => $self_url . '/pid'],
Chris@18 208 'self' => ['href' => $self_url . '/relationships/pid'],
Chris@18 209 ],
Chris@18 210 ],
Chris@18 211 ],
Chris@18 212 ],
Chris@18 213 ];
Chris@18 214 }
Chris@18 215
Chris@18 216 /**
Chris@18 217 * {@inheritdoc}
Chris@18 218 */
Chris@18 219 protected function getPostDocument() {
Chris@18 220 return [
Chris@18 221 'data' => [
Chris@18 222 'type' => 'comment--comment',
Chris@18 223 'attributes' => [
Chris@18 224 'entity_type' => 'entity_test',
Chris@18 225 'field_name' => 'comment',
Chris@18 226 'subject' => 'Dramallama',
Chris@18 227 'comment_body' => [
Chris@18 228 'value' => 'Llamas are awesome.',
Chris@18 229 'format' => 'plain_text',
Chris@18 230 ],
Chris@18 231 ],
Chris@18 232 'relationships' => [
Chris@18 233 'entity_id' => [
Chris@18 234 'data' => [
Chris@18 235 'type' => 'entity_test--bar',
Chris@18 236 'id' => EntityTest::load(1)->uuid(),
Chris@18 237 ],
Chris@18 238 ],
Chris@18 239 ],
Chris@18 240 ],
Chris@18 241 ];
Chris@18 242 }
Chris@18 243
Chris@18 244 /**
Chris@18 245 * {@inheritdoc}
Chris@18 246 */
Chris@18 247 protected function getExpectedCacheTags(array $sparse_fieldset = NULL) {
Chris@18 248 $tags = parent::getExpectedCacheTags($sparse_fieldset);
Chris@18 249 if ($sparse_fieldset === NULL || in_array('comment_body', $sparse_fieldset)) {
Chris@18 250 $tags = Cache::mergeTags($tags, ['config:filter.format.plain_text']);
Chris@18 251 }
Chris@18 252 return $tags;
Chris@18 253 }
Chris@18 254
Chris@18 255 /**
Chris@18 256 * {@inheritdoc}
Chris@18 257 */
Chris@18 258 protected function getExpectedCacheContexts(array $sparse_fieldset = NULL) {
Chris@18 259 $contexts = parent::getExpectedCacheContexts($sparse_fieldset);
Chris@18 260 if ($sparse_fieldset === NULL || in_array('comment_body', $sparse_fieldset)) {
Chris@18 261 $contexts = Cache::mergeContexts($contexts, ['languages:language_interface', 'theme']);
Chris@18 262 }
Chris@18 263 return $contexts;
Chris@18 264 }
Chris@18 265
Chris@18 266 /**
Chris@18 267 * {@inheritdoc}
Chris@18 268 */
Chris@18 269 protected function getExpectedUnauthorizedAccessMessage($method) {
Chris@18 270 switch ($method) {
Chris@18 271 case 'GET';
Chris@18 272 return "The 'access comments' permission is required and the comment must be published.";
Chris@18 273
Chris@18 274 case 'POST';
Chris@18 275 return "The 'post comments' permission is required.";
Chris@18 276
Chris@18 277 case 'PATCH':
Chris@18 278 return "The 'edit own comments' permission is required, the user must be the comment author, and the comment must be published.";
Chris@18 279
Chris@18 280 default:
Chris@18 281 return parent::getExpectedUnauthorizedAccessMessage($method);
Chris@18 282 }
Chris@18 283 }
Chris@18 284
Chris@18 285 /**
Chris@18 286 * Tests POSTing a comment without critical base fields.
Chris@18 287 *
Chris@18 288 * Note that testPostIndividual() is testing with the most minimal
Chris@18 289 * normalization possible: the one returned by ::getNormalizedPostEntity().
Chris@18 290 *
Chris@18 291 * But Comment entities have some very special edge cases:
Chris@18 292 * - base fields that are not marked as required in
Chris@18 293 * \Drupal\comment\Entity\Comment::baseFieldDefinitions() yet in fact are
Chris@18 294 * required.
Chris@18 295 * - base fields that are marked as required, but yet can still result in
Chris@18 296 * validation errors other than "missing required field".
Chris@18 297 */
Chris@18 298 public function testPostIndividualDxWithoutCriticalBaseFields() {
Chris@18 299 $this->setUpAuthorization('POST');
Chris@18 300 $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
Chris@18 301
Chris@18 302 $url = Url::fromRoute(sprintf('jsonapi.%s.collection.post', static::$resourceTypeName));
Chris@18 303 $request_options = [];
Chris@18 304 $request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
Chris@18 305 $request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';
Chris@18 306 $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
Chris@18 307
Chris@18 308 $remove_field = function (array $normalization, $type, $attribute_name) {
Chris@18 309 unset($normalization['data'][$type][$attribute_name]);
Chris@18 310 return $normalization;
Chris@18 311 };
Chris@18 312
Chris@18 313 // DX: 422 when missing 'entity_type' field.
Chris@18 314 $request_options[RequestOptions::BODY] = Json::encode($remove_field($this->getPostDocument(), 'attributes', 'entity_type'));
Chris@18 315 $response = $this->request('POST', $url, $request_options);
Chris@18 316 $this->assertResourceErrorResponse(422, 'entity_type: This value should not be null.', NULL, $response, '/data/attributes/entity_type');
Chris@18 317
Chris@18 318 // DX: 422 when missing 'entity_id' field.
Chris@18 319 $request_options[RequestOptions::BODY] = Json::encode($remove_field($this->getPostDocument(), 'relationships', 'entity_id'));
Chris@18 320 // @todo Remove the try/catch in https://www.drupal.org/node/2820364.
Chris@18 321 try {
Chris@18 322 $response = $this->request('POST', $url, $request_options);
Chris@18 323 $this->assertResourceErrorResponse(422, 'entity_id: This value should not be null.', NULL, $response, '/data/attributes/entity_id');
Chris@18 324 }
Chris@18 325 catch (\Exception $e) {
Chris@18 326 if (version_compare(phpversion(), '7.0') >= 0) {
Chris@18 327 $this->assertSame("Error: Call to a member function get() on null\nDrupal\\comment\\Plugin\\Validation\\Constraint\\CommentNameConstraintValidator->getAnonymousContactDetailsSetting()() (Line: 96)\n", $e->getMessage());
Chris@18 328 }
Chris@18 329 else {
Chris@18 330 $this->assertSame(500, $response->getStatusCode());
Chris@18 331 }
Chris@18 332 }
Chris@18 333
Chris@18 334 // DX: 422 when missing 'field_name' field.
Chris@18 335 $request_options[RequestOptions::BODY] = Json::encode($remove_field($this->getPostDocument(), 'attributes', 'field_name'));
Chris@18 336 $response = $this->request('POST', $url, $request_options);
Chris@18 337 $this->assertResourceErrorResponse(422, 'field_name: This value should not be null.', NULL, $response, '/data/attributes/field_name');
Chris@18 338 }
Chris@18 339
Chris@18 340 /**
Chris@18 341 * Tests POSTing a comment with and without 'skip comment approval'.
Chris@18 342 */
Chris@18 343 public function testPostIndividualSkipCommentApproval() {
Chris@18 344 $this->setUpAuthorization('POST');
Chris@18 345 $this->config('jsonapi.settings')->set('read_only', FALSE)->save(TRUE);
Chris@18 346
Chris@18 347 // Create request.
Chris@18 348 $request_options = [];
Chris@18 349 $request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
Chris@18 350 $request_options[RequestOptions::HEADERS]['Content-Type'] = 'application/vnd.api+json';
Chris@18 351 $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
Chris@18 352 $request_options[RequestOptions::BODY] = Json::encode($this->getPostDocument());
Chris@18 353
Chris@18 354 $url = Url::fromRoute('jsonapi.comment--comment.collection.post');
Chris@18 355
Chris@18 356 // Status should be FALSE when posting as anonymous.
Chris@18 357 $response = $this->request('POST', $url, $request_options);
Chris@18 358 $this->assertResourceResponse(201, FALSE, $response);
Chris@18 359 $this->assertFalse(Json::decode((string) $response->getBody())['data']['attributes']['status']);
Chris@18 360 $this->assertFalse($this->entityStorage->loadUnchanged(2)->isPublished());
Chris@18 361
Chris@18 362 // Grant anonymous permission to skip comment approval.
Chris@18 363 $this->grantPermissionsToTestedRole(['skip comment approval']);
Chris@18 364
Chris@18 365 // Status must be TRUE when posting as anonymous and skip comment approval.
Chris@18 366 $response = $this->request('POST', $url, $request_options);
Chris@18 367 $this->assertResourceResponse(201, FALSE, $response);
Chris@18 368 $this->assertTrue(Json::decode((string) $response->getBody())['data']['attributes']['status']);
Chris@18 369 $this->assertTrue($this->entityStorage->loadUnchanged(3)->isPublished());
Chris@18 370 }
Chris@18 371
Chris@18 372 /**
Chris@18 373 * {@inheritdoc}
Chris@18 374 */
Chris@18 375 protected function getExpectedUnauthorizedAccessCacheability() {
Chris@18 376 // @see \Drupal\comment\CommentAccessControlHandler::checkAccess()
Chris@18 377 return parent::getExpectedUnauthorizedAccessCacheability()
Chris@18 378 ->addCacheTags(['comment:1']);
Chris@18 379 }
Chris@18 380
Chris@18 381 /**
Chris@18 382 * {@inheritdoc}
Chris@18 383 */
Chris@18 384 protected static function entityAccess(EntityInterface $entity, $operation, AccountInterface $account) {
Chris@18 385 // Also reset the 'entity_test' entity access control handler because
Chris@18 386 // comment access also depends on access to the commented entity type.
Chris@18 387 \Drupal::entityTypeManager()->getAccessControlHandler('entity_test')->resetCache();
Chris@18 388 return parent::entityAccess($entity, $operation, $account);
Chris@18 389 }
Chris@18 390
Chris@18 391 /**
Chris@18 392 * {@inheritdoc}
Chris@18 393 */
Chris@18 394 public function testRelated() {
Chris@18 395 $this->markTestSkipped('Remove this in https://www.drupal.org/project/jsonapi/issues/2940339');
Chris@18 396 }
Chris@18 397
Chris@18 398 /**
Chris@18 399 * {@inheritdoc}
Chris@18 400 */
Chris@18 401 protected static function getIncludePermissions() {
Chris@18 402 return [
Chris@18 403 'type' => ['administer comment types'],
Chris@18 404 'uid' => ['access user profiles'],
Chris@18 405 ];
Chris@18 406 }
Chris@18 407
Chris@18 408 /**
Chris@18 409 * {@inheritdoc}
Chris@18 410 */
Chris@18 411 public function testCollectionFilterAccess() {
Chris@18 412 // Verify the expected behavior in the common case.
Chris@18 413 $this->doTestCollectionFilterAccessForPublishableEntities('subject', 'access comments', 'administer comments');
Chris@18 414
Chris@18 415 $collection_url = Url::fromRoute('jsonapi.entity_test--bar.collection');
Chris@18 416 $request_options = [];
Chris@18 417 $request_options[RequestOptions::HEADERS]['Accept'] = 'application/vnd.api+json';
Chris@18 418 $request_options = NestedArray::mergeDeep($request_options, $this->getAuthenticationRequestOptions());
Chris@18 419
Chris@18 420 // Go back to a simpler scenario: revoke the admin permission, publish the
Chris@18 421 // comment and uninstall the query access test module.
Chris@18 422 $this->revokePermissionsFromTestedRole(['administer comments']);
Chris@18 423 $this->entity->setPublished()->save();
Chris@18 424 $this->assertTrue($this->container->get('module_installer')->uninstall(['jsonapi_test_field_filter_access'], TRUE), 'Uninstalled modules.');
Chris@18 425 // ?filter[spotlight.LABEL]: 1 result. Just as already tested above in
Chris@18 426 // ::doTestCollectionFilterAccessForPublishableEntities().
Chris@18 427 $collection_filter_url = $collection_url->setOption('query', ["filter[spotlight.subject]" => $this->entity->label()]);
Chris@18 428 $response = $this->request('GET', $collection_filter_url, $request_options);
Chris@18 429 $doc = Json::decode((string) $response->getBody());
Chris@18 430 $this->assertCount(1, $doc['data']);
Chris@18 431 // Mark the commented entity as inaccessible.
Chris@18 432 \Drupal::state()->set('jsonapi__entity_test_filter_access_blacklist', [$this->entity->getCommentedEntityId()]);
Chris@18 433 Cache::invalidateTags(['state:jsonapi__entity_test_filter_access_blacklist']);
Chris@18 434 // ?filter[spotlight.LABEL]: 0 results.
Chris@18 435 $response = $this->request('GET', $collection_filter_url, $request_options);
Chris@18 436 $doc = Json::decode((string) $response->getBody());
Chris@18 437 $this->assertCount(0, $doc['data']);
Chris@18 438 }
Chris@18 439
Chris@18 440 /**
Chris@18 441 * {@inheritdoc}
Chris@18 442 */
Chris@18 443 protected static function getExpectedCollectionCacheability(AccountInterface $account, array $collection, array $sparse_fieldset = NULL, $filtered = FALSE) {
Chris@18 444 $cacheability = parent::getExpectedCollectionCacheability($account, $collection, $sparse_fieldset, $filtered);
Chris@18 445 if ($filtered) {
Chris@18 446 $cacheability->addCacheTags(['state:jsonapi__entity_test_filter_access_blacklist']);
Chris@18 447 }
Chris@18 448 return $cacheability;
Chris@18 449 }
Chris@18 450
Chris@18 451 /**
Chris@18 452 * {@inheritdoc}
Chris@18 453 */
Chris@18 454 public function testPatchIndividual() {
Chris@18 455 // Ensure ::getModifiedEntityForPatchTesting() can pick an alternative value
Chris@18 456 // for the 'entity_id' field.
Chris@18 457 EntityTest::create([
Chris@18 458 'name' => $this->randomString(),
Chris@18 459 'type' => 'bar',
Chris@18 460 ])->save();
Chris@18 461
Chris@18 462 return parent::testPatchIndividual();
Chris@18 463 }
Chris@18 464
Chris@18 465 }