3 namespace Drupal\Tests\rest\Functional\EntityResource\User;
6 use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
7 use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
8 use Drupal\user\Entity\User;
9 use GuzzleHttp\RequestOptions;
11 abstract class UserResourceTestBase extends EntityResourceTestBase {
13 use BcTimestampNormalizerUnixTestTrait;
18 public static $modules = ['user'];
23 protected static $entityTypeId = 'user';
28 protected static $patchProtectedFieldNames = [
33 * @var \Drupal\user\UserInterface
40 protected static $labelFieldName = 'name';
45 protected static $firstCreatedEntityId = 4;
50 protected static $secondCreatedEntityId = 5;
55 protected function setUpAuthorization($method) {
58 $this->grantPermissionsToTestedRole(['access user profiles']);
63 $this->grantPermissionsToTestedRole(['administer users']);
71 protected function createEntity() {
72 // Create a "Llama" user.
73 $user = User::create(['created' => 123456789]);
74 $user->setUsername('Llama')
75 ->setChangedTime(123456789)
85 protected function createAnotherEntity() {
86 /** @var \Drupal\user\UserInterface $user */
87 $user = $this->entity->createDuplicate();
88 $user->setUsername($user->label() . '_dupe');
96 protected function getExpectedNormalizedEntity() {
102 ['value' => $this->entity->uuid()],
115 $this->formatExpectedTimestampItemValues(123456789),
118 $this->formatExpectedTimestampItemValues($this->entity->getChangedTime()),
120 'default_langcode' => [
131 protected function getNormalizedPostEntity() {
135 'value' => 'Dramallama',
142 * Tests PATCHing security-sensitive base fields of the logged in account.
144 public function testPatchDxForSecuritySensitiveBaseFields() {
145 // The anonymous user is never allowed to modify itself.
146 if (!static::$auth) {
147 $this->markTestSkipped();
150 $this->initAuthentication();
151 $this->provisionEntityResource();
153 /** @var \Drupal\user\UserInterface $user */
154 $user = static::$auth ? $this->account : User::load(0);
155 // @todo Remove the array_diff_key() call in https://www.drupal.org/node/2821077.
156 $original_normalization = array_diff_key($this->serializer->normalize($user, static::$format), ['created' => TRUE, 'changed' => TRUE, 'name' => TRUE]);
158 // Since this test must be performed by the user that is being modified,
159 // we cannot use $this->getUrl().
160 $url = $user->toUrl()->setOption('query', ['_format' => static::$format]);
162 RequestOptions::HEADERS => ['Content-Type' => static::$mimeType],
164 $request_options = array_merge_recursive($request_options, $this->getAuthenticationRequestOptions('PATCH'));
166 // Test case 1: changing email.
167 $normalization = $original_normalization;
168 $normalization['mail'] = [['value' => 'new-email@example.com']];
169 $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
171 // DX: 422 when changing email without providing the password.
172 $response = $this->request('PATCH', $url, $request_options);
173 $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n", $response, FALSE, FALSE, FALSE, FALSE);
175 $normalization['pass'] = [['existing' => 'wrong']];
176 $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
178 // DX: 422 when changing email while providing a wrong password.
179 $response = $this->request('PATCH', $url, $request_options);
180 $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\nmail: Your current password is missing or incorrect; it's required to change the Email.\n", $response, FALSE, FALSE, FALSE, FALSE);
182 $normalization['pass'] = [['existing' => $this->account->passRaw]];
183 $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
185 // 200 for well-formed request.
186 $response = $this->request('PATCH', $url, $request_options);
187 $this->assertResourceResponse(200, FALSE, $response);
189 // Test case 2: changing password.
190 $normalization = $original_normalization;
191 $new_password = $this->randomString();
192 $normalization['pass'] = [['value' => $new_password]];
193 $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
195 // DX: 422 when changing password without providing the current password.
196 $response = $this->request('PATCH', $url, $request_options);
197 $this->assertResourceErrorResponse(422, "Unprocessable Entity: validation failed.\npass: Your current password is missing or incorrect; it's required to change the Password.\n", $response, FALSE, FALSE, FALSE, FALSE);
199 $normalization['pass'][0]['existing'] = $this->account->pass_raw;
200 $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
202 // 200 for well-formed request.
203 $response = $this->request('PATCH', $url, $request_options);
204 $this->assertResourceResponse(200, FALSE, $response);
206 // Verify that we can log in with the new password.
207 $this->assertRpcLogin($user->getAccountName(), $new_password);
209 // Update password in $this->account, prepare for future requests.
210 $this->account->passRaw = $new_password;
211 $this->initAuthentication();
213 RequestOptions::HEADERS => ['Content-Type' => static::$mimeType],
215 $request_options = array_merge_recursive($request_options, $this->getAuthenticationRequestOptions('PATCH'));
217 // Test case 3: changing name.
218 $normalization = $original_normalization;
219 $normalization['name'] = [['value' => 'Cooler Llama']];
220 $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
222 // DX: 403 when modifying username without required permission.
223 $response = $this->request('PATCH', $url, $request_options);
224 $this->assertResourceErrorResponse(403, "Access denied on updating field 'name'.", $response);
226 $this->grantPermissionsToTestedRole(['change own username']);
228 // 200 for well-formed request.
229 $response = $this->request('PATCH', $url, $request_options);
230 $this->assertResourceResponse(200, FALSE, $response);
232 // Verify that we can log in with the new username.
233 $this->assertRpcLogin('Cooler Llama', $new_password);
237 * Verifies that logging in with the given username and password works.
239 * @param string $username
240 * The username to log in with.
241 * @param string $password
242 * The password to log in with.
244 protected function assertRpcLogin($username, $password) {
250 RequestOptions::HEADERS => [],
251 RequestOptions::BODY => $this->serializer->encode($request_body, 'json'),
253 $response = $this->request('POST', Url::fromRoute('user.login.http')->setRouteParameter('_format', 'json'), $request_options);
254 $this->assertSame(200, $response->getStatusCode());
258 * Tests PATCHing security-sensitive base fields to change other users.
260 public function testPatchSecurityOtherUser() {
261 // The anonymous user is never allowed to modify other users.
262 if (!static::$auth) {
263 $this->markTestSkipped();
266 $this->initAuthentication();
267 $this->provisionEntityResource();
269 /** @var \Drupal\user\UserInterface $user */
270 $user = $this->account;
271 $original_normalization = array_diff_key($this->serializer->normalize($user, static::$format), ['changed' => TRUE]);
273 // Since this test must be performed by the user that is being modified,
274 // we cannot use $this->getUrl().
275 $url = $user->toUrl()->setOption('query', ['_format' => static::$format]);
277 RequestOptions::HEADERS => ['Content-Type' => static::$mimeType],
279 $request_options = array_merge_recursive($request_options, $this->getAuthenticationRequestOptions('PATCH'));
281 $normalization = $original_normalization;
282 $normalization['mail'] = [['value' => 'new-email@example.com']];
283 $request_options[RequestOptions::BODY] = $this->serializer->encode($normalization, static::$format);
285 // Try changing user 1's email.
287 'mail' => [['value' => 'another_email_address@example.com']],
288 'uid' => [['value' => 1]],
289 'name' => [['value' => 'another_user_name']],
290 'pass' => [['existing' => $this->account->passRaw]],
291 'uuid' => [['value' => '2e9403a4-d8af-4096-a116-624710140be0']],
292 ] + $original_normalization;
293 $request_options[RequestOptions::BODY] = $this->serializer->encode($user1, static::$format);
294 $response = $this->request('PATCH', $url, $request_options);
295 // Ensure the email address has not changed.
296 $this->assertEquals('admin@example.com', $this->entityStorage->loadUnchanged(1)->getEmail());
297 $this->assertResourceErrorResponse(403, "Access denied on updating field 'uid'.", $response);
303 protected function getExpectedUnauthorizedAccessMessage($method) {
304 if ($this->config('rest.settings')->get('bc_entity_resource_permissions')) {
305 return parent::getExpectedUnauthorizedAccessMessage($method);
310 return "The 'access user profiles' permission is required and the user must be active.";
312 return "You are not authorized to update this user entity.";
314 return 'You are not authorized to delete this user entity.';
316 return parent::getExpectedUnauthorizedAccessMessage($method);
323 protected function getExpectedUnauthorizedAccessCacheability() {
324 // @see \Drupal\user\UserAccessControlHandler::checkAccess()
325 return parent::getExpectedUnauthorizedAccessCacheability()
326 ->addCacheTags(['user:3']);