summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authoreffulgentsia2016-08-03 17:28:52 (GMT)
committereffulgentsia2016-08-03 17:28:52 (GMT)
commit79dfc068a0bd40e1ffe9cfba785397468d239bfa (patch)
treefa57093283bf54dd4282d994a70028f331a7498b
parent97f20479878617cf76c210c567ab7cf99262fb5f (diff)
Issue #2291055 by marthinal, tedbow, Wim Leers, kylebrowning, m1r1k, clemens.tolboom, jlbellido, vivekvpandya, snehal.brahmbhatt, dawehner, klausi, droti, alexpott, cloudbull, Berdir, heykarthikwithu, claudiu.cristea: REST resources for anonymous users: register
-rw-r--r--core/modules/rest/src/Plugin/rest/resource/EntityResource.php48
-rw-r--r--core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php35
-rw-r--r--core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php44
-rw-r--r--core/modules/rest/tests/src/Unit/EntityResourceValidationTraitTest.php73
-rw-r--r--core/modules/user/src/Plugin/rest/resource/UserRegistrationResource.php190
-rw-r--r--core/modules/user/src/Tests/RestRegisterUserTest.php187
-rw-r--r--core/modules/user/tests/src/Unit/UserRegistrationResourceTest.php151
7 files changed, 685 insertions, 43 deletions
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
index 5cf42dd..dfb0272 100644
--- a/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResource.php
@@ -36,6 +36,9 @@ use Symfony\Component\HttpKernel\Exception\HttpException;
*/
class EntityResource extends ResourceBase implements DependentPluginInterface {
+ use EntityResourceValidationTrait;
+ use EntityResourceAccessTrait;
+
/**
* The entity type targeted by this resource.
*
@@ -156,14 +159,7 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
throw new BadRequestHttpException('Only new entities can be created');
}
- // Only check 'edit' permissions for fields that were actually
- // submitted by the user. Field access makes no difference between 'create'
- // and 'update', so the 'edit' operation is used here.
- foreach ($entity->_restSubmittedFields as $key => $field_name) {
- if (!$entity->get($field_name)->access('edit')) {
- throw new AccessDeniedHttpException("Access denied on creating field '$field_name'");
- }
- }
+ $this->checkEditFieldAccess($entity);
// Validate the received data before saving.
$this->validate($entity);
@@ -175,8 +171,7 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
// body. These responses are not cacheable, so we add no cacheability
// metadata here.
$url = $entity->urlInfo('canonical', ['absolute' => TRUE])->toString(TRUE);
- $response = new ModifiedResourceResponse($entity, 201, ['Location' => $url->getGeneratedUrl()]);
- return $response;
+ return new ModifiedResourceResponse($entity, 201, ['Location' => $url->getGeneratedUrl()]);
}
catch (EntityStorageException $e) {
throw new HttpException(500, 'Internal Server Error', $e);
@@ -277,39 +272,6 @@ class EntityResource extends ResourceBase implements DependentPluginInterface {
}
/**
- * Verifies that the whole entity does not violate any validation constraints.
- *
- * @param \Drupal\Core\Entity\EntityInterface $entity
- * The entity object.
- *
- * @throws \Symfony\Component\HttpKernel\Exception\HttpException
- * If validation errors are found.
- */
- protected function validate(EntityInterface $entity) {
- // @todo Remove when https://www.drupal.org/node/2164373 is committed.
- if (!$entity instanceof FieldableEntityInterface) {
- return;
- }
- $violations = $entity->validate();
-
- // Remove violations of inaccessible fields as they cannot stem from our
- // changes.
- $violations->filterByFieldAccess();
-
- if (count($violations) > 0) {
- $message = "Unprocessable Entity: validation failed.\n";
- foreach ($violations as $violation) {
- $message .= $violation->getPropertyPath() . ': ' . $violation->getMessage() . "\n";
- }
- // Instead of returning a generic 400 response we use the more specific
- // 422 Unprocessable Entity code from RFC 4918. That way clients can
- // distinguish between general syntax errors in bad serializations (code
- // 400) and semantic errors in well-formed requests (code 422).
- throw new HttpException(422, $message);
- }
- }
-
- /**
* {@inheritdoc}
*/
public function permissions() {
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php b/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php
new file mode 100644
index 0000000..7bf8e82
--- /dev/null
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResourceAccessTrait.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Drupal\rest\Plugin\rest\resource;
+
+use Drupal\Core\Entity\EntityInterface;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+
+/**
+ * @internal
+ * @todo Consider making public in https://www.drupal.org/node/2300677
+ */
+trait EntityResourceAccessTrait {
+
+ /**
+ * Performs edit access checks for fields.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity whose fields edit access should be checked for.
+ *
+ * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
+ * Throws access denied when the user does not have permissions to edit a
+ * field.
+ */
+ protected function checkEditFieldAccess(EntityInterface $entity) {
+ // Only check 'edit' permissions for fields that were actually submitted by
+ // the user. Field access makes no difference between 'create' and 'update',
+ // so the 'edit' operation is used here.
+ foreach ($entity->_restSubmittedFields as $key => $field_name) {
+ if (!$entity->get($field_name)->access('edit')) {
+ throw new AccessDeniedHttpException("Access denied on creating field '$field_name'.");
+ }
+ }
+ }
+
+}
diff --git a/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php
new file mode 100644
index 0000000..a2ff40a
--- /dev/null
+++ b/core/modules/rest/src/Plugin/rest/resource/EntityResourceValidationTrait.php
@@ -0,0 +1,44 @@
+<?php
+
+namespace Drupal\rest\Plugin\rest\resource;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Entity\FieldableEntityInterface;
+use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
+
+/**
+ * @internal
+ * @todo Consider making public in https://www.drupal.org/node/2300677
+ */
+trait EntityResourceValidationTrait {
+
+ /**
+ * Verifies that the whole entity does not violate any validation constraints.
+ *
+ * @param \Drupal\Core\Entity\EntityInterface $entity
+ * The entity to validate.
+ *
+ * @throws \Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException
+ * If validation errors are found.
+ */
+ protected function validate(EntityInterface $entity) {
+ // @todo Remove when https://www.drupal.org/node/2164373 is committed.
+ if (!$entity instanceof FieldableEntityInterface) {
+ return;
+ }
+ $violations = $entity->validate();
+
+ // Remove violations of inaccessible fields as they cannot stem from our
+ // changes.
+ $violations->filterByFieldAccess();
+
+ if ($violations->count() > 0) {
+ $message = "Unprocessable Entity: validation failed.\n";
+ foreach ($violations as $violation) {
+ $message .= $violation->getPropertyPath() . ': ' . $violation->getMessage() . "\n";
+ }
+ throw new UnprocessableEntityHttpException($message);
+ }
+ }
+
+}
diff --git a/core/modules/rest/tests/src/Unit/EntityResourceValidationTraitTest.php b/core/modules/rest/tests/src/Unit/EntityResourceValidationTraitTest.php
new file mode 100644
index 0000000..20a6175
--- /dev/null
+++ b/core/modules/rest/tests/src/Unit/EntityResourceValidationTraitTest.php
@@ -0,0 +1,73 @@
+<?php
+
+namespace Drupal\Tests\rest\Unit;
+
+use Drupal\Core\Entity\EntityConstraintViolationList;
+use Drupal\node\Entity\Node;
+use Drupal\Tests\UnitTestCase;
+use Drupal\user\Entity\User;
+use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
+use Symfony\Component\Validator\ConstraintViolationInterface;
+
+/**
+ * @group rest
+ * @coversDefaultClass \Drupal\rest\Plugin\rest\resource\EntityResourceValidationTrait
+ */
+class EntityResourceValidationTraitTest extends UnitTestCase {
+
+ /**
+ * @covers ::validate
+ */
+ public function testValidate() {
+ $trait = $this->getMockForTrait('Drupal\rest\Plugin\rest\resource\EntityResourceValidationTrait');
+
+ $method = new \ReflectionMethod($trait, 'validate');
+ $method->setAccessible(TRUE);
+
+ $entity = $this->prophesize(Node::class);
+
+ $violations = $this->prophesize(EntityConstraintViolationList::class);
+ $violations->filterByFieldAccess()->willReturn([]);
+ $violations->count()->willReturn(0);
+
+ $entity->validate()->willReturn($violations->reveal());
+
+ $method->invoke($trait, $entity->reveal());
+ }
+
+ /**
+ * @covers ::validate
+ */
+ public function testFailedValidate() {
+ $violation1 = $this->prophesize(ConstraintViolationInterface::class);
+ $violation1->getPropertyPath()->willReturn('property_path');
+ $violation1->getMessage()->willReturn('message');
+
+ $violation2 = $this->prophesize(ConstraintViolationInterface::class);
+ $violation2->getPropertyPath()->willReturn('property_path');
+ $violation2->getMessage()->willReturn('message');
+
+ $entity = $this->prophesize(User::class);
+
+ $violations = $this->getMockBuilder(EntityConstraintViolationList::class)
+ ->setConstructorArgs([$entity->reveal(), [$violation1->reveal(), $violation2->reveal()]])
+ ->setMethods(['filterByFieldAccess'])
+ ->getMock();
+
+ $violations->expects($this->once())
+ ->method('filterByFieldAccess')
+ ->will($this->returnValue([]));
+
+ $entity->validate()->willReturn($violations);
+
+ $trait = $this->getMockForTrait('Drupal\rest\Plugin\rest\resource\EntityResourceValidationTrait');
+
+ $method = new \ReflectionMethod($trait, 'validate');
+ $method->setAccessible(TRUE);
+
+ $this->setExpectedException(UnprocessableEntityHttpException::class);
+
+ $method->invoke($trait, $entity->reveal());
+ }
+
+}
diff --git a/core/modules/user/src/Plugin/rest/resource/UserRegistrationResource.php b/core/modules/user/src/Plugin/rest/resource/UserRegistrationResource.php
new file mode 100644
index 0000000..6a243c3
--- /dev/null
+++ b/core/modules/user/src/Plugin/rest/resource/UserRegistrationResource.php
@@ -0,0 +1,190 @@
+<?php
+
+namespace Drupal\user\Plugin\rest\resource;
+
+use Drupal\Core\Config\ImmutableConfig;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\rest\ModifiedResourceResponse;
+use Drupal\rest\Plugin\ResourceBase;
+use Drupal\rest\Plugin\rest\resource\EntityResourceAccessTrait;
+use Drupal\rest\Plugin\rest\resource\EntityResourceValidationTrait;
+use Drupal\user\UserInterface;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\DependencyInjection\ContainerInterface;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+use Symfony\Component\HttpKernel\Exception\UnprocessableEntityHttpException;
+
+/**
+ * Represents user registration as a resource.
+ *
+ * @RestResource(
+ * id = "user_registration",
+ * label = @Translation("User registration"),
+ * serialization_class = "Drupal\user\Entity\User",
+ * uri_paths = {
+ * "https://www.drupal.org/link-relations/create" = "/user/register",
+ * },
+ * )
+ */
+class UserRegistrationResource extends ResourceBase {
+
+ use EntityResourceValidationTrait;
+ use EntityResourceAccessTrait;
+
+ /**
+ * User settings config instance.
+ *
+ * @var \Drupal\Core\Config\ImmutableConfig
+ */
+ protected $userSettings;
+
+ /**
+ * The current user.
+ *
+ * @var \Drupal\Core\Session\AccountInterface
+ */
+ protected $currentUser;
+
+ /**
+ * Constructs a new UserRegistrationResource instance.
+ *
+ * @param array $configuration
+ * A configuration array containing information about the plugin instance.
+ * @param string $plugin_id
+ * The plugin_id for the plugin instance.
+ * @param mixed $plugin_definition
+ * The plugin implementation definition.
+ * @param array $serializer_formats
+ * The available serialization formats.
+ * @param \Psr\Log\LoggerInterface $logger
+ * A logger instance.
+ * @param \Drupal\Core\Config\ImmutableConfig $user_settings
+ * A user settings config instance.
+ * @param \Drupal\Core\Session\AccountInterface $current_user
+ * The current user.
+ */
+ public function __construct(array $configuration, $plugin_id, $plugin_definition, array $serializer_formats, LoggerInterface $logger, ImmutableConfig $user_settings, AccountInterface $current_user) {
+ parent::__construct($configuration, $plugin_id, $plugin_definition, $serializer_formats, $logger);
+ $this->userSettings = $user_settings;
+ $this->currentUser = $current_user;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
+ return new static(
+ $configuration,
+ $plugin_id,
+ $plugin_definition,
+ $container->getParameter('serializer.formats'),
+ $container->get('logger.factory')->get('rest'),
+ $container->get('config.factory')->get('user.settings'),
+ $container->get('current_user')
+ );
+ }
+
+ /**
+ * Responds to user registration POST request.
+ *
+ * @param \Drupal\user\UserInterface $account
+ * The user account entity.
+ *
+ * @return \Drupal\rest\ModifiedResourceResponse
+ * The HTTP response object.
+ *
+ * @throws \Symfony\Component\HttpKernel\Exception\BadRequestHttpException
+ * @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
+ */
+ public function post(UserInterface $account = NULL) {
+ $this->ensureAccountCanRegister($account);
+
+ // Only activate new users if visitors are allowed to register and no email
+ // verification required.
+ if ($this->userSettings->get('register') == USER_REGISTER_VISITORS && !$this->userSettings->get('verify_mail')) {
+ $account->activate();
+ }
+ else {
+ $account->block();
+ }
+
+ $this->checkEditFieldAccess($account);
+
+ // Make sure that the user entity is valid (email and name are valid).
+ $this->validate($account);
+
+ // Create the account.
+ $account->save();
+
+ $this->sendEmailNotifications($account);
+
+ return new ModifiedResourceResponse($account, 200);
+ }
+
+ /**
+ * Ensure the account can be registered in this request.
+ *
+ * @param \Drupal\user\UserInterface $account
+ * The user account to register.
+ */
+ protected function ensureAccountCanRegister(UserInterface $account = NULL) {
+ if ($account === NULL) {
+ throw new BadRequestHttpException('No user account data for registration received.');
+ }
+
+ // POSTed user accounts must not have an ID set, because we always want to
+ // create new entities here.
+ if (!$account->isNew()) {
+ throw new BadRequestHttpException('An ID has been set and only new user accounts can be registered.');
+ }
+
+ // Only allow anonymous users to register, authenticated users with the
+ // necessary permissions can POST a new user to the "user" REST resource.
+ // @see \Drupal\rest\Plugin\rest\resource\EntityResource
+ if (!$this->currentUser->isAnonymous()) {
+ throw new AccessDeniedHttpException('Only anonymous users can register a user.');
+ }
+
+ // Verify that the current user can register a user account.
+ if ($this->userSettings->get('register') == USER_REGISTER_ADMINISTRATORS_ONLY) {
+ throw new AccessDeniedHttpException('You cannot register a new user account.');
+ }
+
+ if (!$this->userSettings->get('verify_mail')) {
+ if (empty($account->getPassword())) {
+ // If no e-mail verification then the user must provide a password.
+ throw new UnprocessableEntityHttpException('No password provided.');
+ }
+ }
+ else {
+ if (!empty($account->getPassword())) {
+ // If e-mail verification required then a password cannot provided.
+ // The password will be set when the user logs in.
+ throw new UnprocessableEntityHttpException('A Password cannot be specified. It will be generated on login.');
+ }
+ }
+ }
+
+ /**
+ * Sends email notifications if necessary for user that was registered.
+ *
+ * @param \Drupal\user\UserInterface $account
+ * The user account.
+ */
+ protected function sendEmailNotifications(UserInterface $account) {
+ $approval_settings = $this->userSettings->get('register');
+ // No e-mail verification is required. Activating the user.
+ if ($approval_settings == USER_REGISTER_VISITORS) {
+ if ($this->userSettings->get('verify_mail')) {
+ // No administrator approval required.
+ _user_mail_notify('register_no_approval_required', $account);
+ }
+ }
+ // Administrator approval required.
+ elseif ($approval_settings == USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL) {
+ _user_mail_notify('register_pending_approval', $account);
+ }
+ }
+
+}
diff --git a/core/modules/user/src/Tests/RestRegisterUserTest.php b/core/modules/user/src/Tests/RestRegisterUserTest.php
new file mode 100644
index 0000000..c10d028
--- /dev/null
+++ b/core/modules/user/src/Tests/RestRegisterUserTest.php
@@ -0,0 +1,187 @@
+<?php
+
+namespace Drupal\user\Tests;
+
+use Drupal\rest\Tests\RESTTestBase;
+use Drupal\user\Entity\Role;
+use Drupal\user\RoleInterface;
+
+/**
+ * Tests user registration via REST resource.
+ *
+ * @group user
+ */
+class RestRegisterUserTest extends RESTTestBase {
+
+ /**
+ * {@inheritdoc}
+ */
+ public static $modules = ['hal'];
+
+ /**
+ * {@inheritdoc}
+ */
+ public function setUp() {
+ parent::setUp();
+
+ $this->enableService('user_registration', 'POST', 'hal_json');
+
+ Role::load(RoleInterface::ANONYMOUS_ID)
+ ->grantPermission('restful post user_registration')
+ ->save();
+
+ Role::load(RoleInterface::AUTHENTICATED_ID)
+ ->grantPermission('restful post user_registration')
+ ->save();
+ }
+
+ /**
+ * Tests that only anonymous users can register users.
+ */
+ public function testRegisterUser() {
+ // Verify that an authenticated user cannot register a new user, despite
+ // being granted permission to do so because only anonymous users can
+ // register themselves, authenticated users with the necessary permissions
+ // can POST a new user to the "user" REST resource.
+ $user = $this->createUser();
+ $this->drupalLogin($user);
+ $this->registerRequest('palmer.eldritch');
+ $this->assertResponse('403', 'Only anonymous users can register users.');
+ $this->drupalLogout();
+
+ $user_settings = $this->config('user.settings');
+
+ // Test out different setting User Registration and Email Verification.
+ // Allow visitors to register with no email verification.
+ $user_settings->set('register', USER_REGISTER_VISITORS);
+ $user_settings->set('verify_mail', 0);
+ $user_settings->save();
+ $user = $this->registerUser('Palmer.Eldritch');
+ $this->assertFalse($user->isBlocked());
+ $this->assertFalse(empty($user->getPassword()));
+ $email_count = count($this->drupalGetMails());
+ $this->assertEqual(0, $email_count);
+
+ // Attempt to register without sending a password.
+ $this->registerRequest('Rick.Deckard', FALSE);
+ $this->assertResponse('422', 'No password provided');
+
+ // Allow visitors to register with email verification.
+ $user_settings->set('register', USER_REGISTER_VISITORS);
+ $user_settings->set('verify_mail', 1);
+ $user_settings->save();
+ $user = $this->registerUser('Jason.Taverner', FALSE);
+ $this->assertTrue(empty($user->getPassword()));
+ $this->assertTrue($user->isBlocked());
+ $this->assertMailString('body', 'You may now log in by clicking this link', 1);
+
+ // Attempt to register with a password when e-mail verification is on.
+ $this->registerRequest('Estraven', TRUE);
+ $this->assertResponse('422', 'A Password cannot be specified. It will be generated on login.');
+
+ // Allow visitors to register with Admin approval and e-mail verification.
+ $user_settings->set('register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL);
+ $user_settings->set('verify_mail', 1);
+ $user_settings->save();
+ $user = $this->registerUser('Bob.Arctor', FALSE);
+ $this->assertTrue(empty($user->getPassword()));
+ $this->assertTrue($user->isBlocked());
+ $this->assertMailString('body', 'Your application for an account is', 2);
+ $this->assertMailString('body', 'Bob.Arctor has applied for an account', 2);
+
+ // Attempt to register with a password when e-mail verification is on.
+ $this->registerRequest('Ursula', TRUE);
+ $this->assertResponse('422', 'A Password cannot be specified. It will be generated on login.');
+
+ // Allow visitors to register with Admin approval and no email verification.
+ $user_settings->set('register', USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL);
+ $user_settings->set('verify_mail', 0);
+ $user_settings->save();
+ $user = $this->registerUser('Argaven');
+ $this->assertFalse(empty($user->getPassword()));
+ $this->assertTrue($user->isBlocked());
+ $this->assertMailString('body', 'Your application for an account is', 2);
+ $this->assertMailString('body', 'Argaven has applied for an account', 2);
+
+ // Attempt to register without sending a password.
+ $this->registerRequest('Tibe', FALSE);
+ $this->assertResponse('422', 'No password provided');
+ }
+
+ /**
+ * Creates serialize user values.
+ *
+ * @param string $name
+ * The name of the user. Use only valid values for emails.
+ *
+ * @param bool $include_password
+ * Whether to include a password in the user values.
+ *
+ * @return string Serialized user values.
+ * Serialized user values.
+ */
+ protected function createSerializedUser($name, $include_password = TRUE) {
+ global $base_url;
+ // New user info to be serialized.
+ $data = [
+ "_links" =>
+ [
+ "type" => ["href" => $base_url . "/rest/type/user/user"],
+ ],
+ "langcode" => [
+ [
+ "value" => "en",
+ ],
+ ],
+ "name" => [
+ [
+ "value" => $name,
+ ],
+ ],
+ "mail" => [
+ [
+ "value" => "$name@example.com",
+ ],
+ ],
+ ];
+ if ($include_password) {
+ $data['pass']['value'] = 'SuperSecretPassword';
+ }
+
+ // Create a HAL+JSON version for the user entity we want to create.
+ $serialized = $this->container->get('serializer')
+ ->serialize($data, 'hal_json');
+ return $serialized;
+ }
+
+ /**
+ * Registers a user via REST resource.
+ *
+ * @param $name
+ * User name.
+ *
+ * @param bool $include_password
+ *
+ * @return bool|\Drupal\user\Entity\User
+ */
+ protected function registerUser($name, $include_password = TRUE) {
+ // Verify that an anonymous user can register.
+ $this->registerRequest($name, $include_password);
+ $this->assertResponse('200', 'HTTP response code is correct.');
+ $user = user_load_by_name($name);
+ $this->assertFalse(empty($user), 'User was create as expected');
+ return $user;
+ }
+
+ /**
+ * Make a REST user registration request.
+ *
+ * @param $name
+ * @param $include_password
+ */
+ protected function registerRequest($name, $include_password = TRUE) {
+ $serialized = $this->createSerializedUser($name, $include_password);
+ $this->httpRequest('/user/register', 'POST', $serialized, 'application/hal+json');
+ }
+
+}
diff --git a/core/modules/user/tests/src/Unit/UserRegistrationResourceTest.php b/core/modules/user/tests/src/Unit/UserRegistrationResourceTest.php
new file mode 100644
index 0000000..142685c
--- /dev/null
+++ b/core/modules/user/tests/src/Unit/UserRegistrationResourceTest.php
@@ -0,0 +1,151 @@
+<?php
+
+namespace Drupal\Tests\user\Unit;
+
+use Drupal\Core\Config\ImmutableConfig;
+use Drupal\Core\Session\AccountInterface;
+use Drupal\Tests\UnitTestCase;
+use Drupal\user\Entity\User;
+use Drupal\user\Plugin\rest\resource\UserRegistrationResource;
+use Psr\Log\LoggerInterface;
+use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
+use Symfony\Component\HttpKernel\Exception\BadRequestHttpException;
+
+/**
+ * Only administrators can create user accounts.
+ */
+if (!defined('USER_REGISTER_ADMINISTRATORS_ONLY')) {
+ define('USER_REGISTER_ADMINISTRATORS_ONLY', 'admin_only');
+}
+
+/**
+ * Visitors can create their own accounts.
+ */
+if (!defined('USER_REGISTER_VISITORS')) {
+ define('USER_REGISTER_VISITORS', 'visitors');
+}
+
+/**
+ * Visitors can create accounts, but they don't become active without
+ * administrative approval.
+ */
+if (!defined('USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL')) {
+ define('USER_REGISTER_VISITORS_ADMINISTRATIVE_APPROVAL', 'visitors_admin_approval');
+}
+
+/**
+ * Tests User Registration REST resource.
+ *
+ * @coversDefaultClass \Drupal\user\Plugin\rest\resource\UserRegistrationResource
+ * @group user
+ */
+class UserRegistrationResourceTest extends UnitTestCase {
+
+ const ERROR_MESSAGE = "Unprocessable Entity: validation failed.\nproperty_path: message\nproperty_path_2: message_2\n";
+
+ /**
+ * Class to be tested.
+ *
+ * @var \Drupal\user\Plugin\rest\resource\UserRegistrationResource
+ */
+ protected $testClass;
+
+ /**
+ * A reflection of self::$testClass.
+ *
+ * @var \ReflectionClass
+ */
+ protected $reflection;
+
+ /**
+ * A user settings config instance.
+ *
+ * @var \Drupal\Core\Config\ImmutableConfig|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $userSettings;
+
+ /**
+ * Logger service.
+ *
+ * @var \Psr\Log\LoggerInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $logger;
+
+ /**
+ * The current user.
+ *
+ * @var \Drupal\Core\Session\AccountInterface|\PHPUnit_Framework_MockObject_MockObject
+ */
+ protected $currentUser;
+
+ /**
+ * {@inheritdoc}
+ */
+ protected function setUp() {
+ parent::setUp();
+
+ $this->logger = $this->prophesize(LoggerInterface::class)->reveal();
+
+ $this->userSettings = $this->prophesize(ImmutableConfig::class);
+
+ $this->currentUser = $this->prophesize(AccountInterface::class);
+
+ $this->testClass = new UserRegistrationResource([], 'plugin_id', '', [], $this->logger, $this->userSettings->reveal(), $this->currentUser->reveal());
+ $this->reflection = new \ReflectionClass($this->testClass);
+ }
+
+ /**
+ * Tests that an exception is thrown when no data provided for the account.
+ */
+ public function testEmptyPost() {
+ $this->setExpectedException(BadRequestHttpException::class);
+ $this->testClass->post(NULL);
+ }
+
+ /**
+ * Tests that only new user accounts can be registered.
+ */
+ public function testExistedEntityPost() {
+ $entity = $this->prophesize(User::class);
+ $entity->isNew()->willReturn(FALSE);
+ $this->setExpectedException(BadRequestHttpException::class);
+
+ $this->testClass->post($entity->reveal());
+ }
+
+ /**
+ * Tests that admin permissions are required to register a user account.
+ */
+ public function testRegistrationAdminOnlyPost() {
+
+ $this->userSettings->get('register')->willReturn(USER_REGISTER_ADMINISTRATORS_ONLY);
+
+ $this->currentUser->isAnonymous()->willReturn(TRUE);
+
+ $this->testClass = new UserRegistrationResource([], 'plugin_id', '', [], $this->logger, $this->userSettings->reveal(), $this->currentUser->reveal());
+
+ $entity = $this->prophesize(User::class);
+ $entity->isNew()->willReturn(TRUE);
+
+ $this->setExpectedException(AccessDeniedHttpException::class);
+
+ $this->testClass->post($entity->reveal());
+ }
+
+ /**
+ * Tests that only anonymous users can register users.
+ */
+ public function testRegistrationAnonymousOnlyPost() {
+ $this->currentUser->isAnonymous()->willReturn(FALSE);
+
+ $this->testClass = new UserRegistrationResource([], 'plugin_id', '', [], $this->logger, $this->userSettings->reveal(), $this->currentUser->reveal());
+
+ $entity = $this->prophesize(User::class);
+ $entity->isNew()->willReturn(TRUE);
+
+ $this->setExpectedException(AccessDeniedHttpException::class);
+
+ $this->testClass->post($entity->reveal());
+ }
+
+}