create(); $response = $this->actingAs($user)->get('/api/client/account'); $response->assertOk()->assertJson([ 'object' => 'user', 'attributes' => [ 'uuid' => $user->uuid, 'username' => $user->username, 'email' => $user->email, 'language' => 'en', 'image' => 'https://gravatar.com/avatar/' . md5(Str::lower($user->email)), 'admin' => false, 'root_admin' => false, '2fa_enabled' => false, 'created_at' => $this->formatTimestamp($user->created_at), 'updated_at' => $this->formatTimestamp($user->updated_at), ], ]); } /** * Test that the user's email address can be updated via the API. */ public function test_email_is_updated(): void { /** @var User $user */ $user = User::factory()->create(); $this->actingAs($user) ->putJson('/api/client/account/email', [ 'email' => $email = mb_strtolower(Str::random() . '@example.com'), 'password' => 'password', ]) ->assertStatus(Response::HTTP_NO_CONTENT); $this->assertActivityFor('user:account.email-changed', $user, $user); $this->assertDatabaseHas('users', ['id' => $user->id, 'email' => $email]); } public function test_email_change_is_throttled(): void { /** @var Collection $users */ $users = User::factory()->count(2)->create(); for ($i = 0; $i < 3; $i++) { $this->actingAs($users[0]) ->putJson('/api/client/account/email', ['email' => "foo+{$i}@example.com", 'password' => 'password']) ->assertNoContent(); } $this->putJson('/api/client/account/email', ['email' => 'bar@example.com', 'password' => 'password']) ->assertTooManyRequests(); // The other user should still be able to update their email because the throttle // is tied to the account, not to the IP address. $this->actingAs($users[1]) ->putJson('/api/client/account/email', ['email' => 'bar+1@example.com', 'password' => 'password']) ->assertNoContent(); } /** * Tests that an email is not updated if the password provided in the request is not * valid for the account. */ public function test_email_is_not_updated_when_password_is_invalid(): void { /** @var User $user */ $user = User::factory()->create(); $response = $this->actingAs($user)->putJson('/api/client/account/email', [ 'email' => 'hodor@example.com', 'password' => 'invalid', ]); $response->assertStatus(Response::HTTP_BAD_REQUEST); $response->assertJsonPath('errors.0.code', 'InvalidPasswordProvidedException'); $response->assertJsonPath('errors.0.detail', 'The password provided was invalid for this account.'); } /** * Tests that an email is not updated if an invalid email address is passed through * in the request. */ public function test_email_is_not_updated_when_not_valid(): void { /** @var User $user */ $user = User::factory()->create(); $response = $this->actingAs($user)->putJson('/api/client/account/email', [ 'email' => '', 'password' => 'password', ]); $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); $response->assertJsonPath('errors.0.meta.rule', 'required'); $response->assertJsonPath('errors.0.detail', 'The email field is required.'); $response = $this->actingAs($user)->putJson('/api/client/account/email', [ 'email' => 'invalid', 'password' => 'password', ]); $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); $response->assertJsonPath('errors.0.meta.rule', 'email'); $response->assertJsonPath('errors.0.detail', 'The email must be a valid email address.'); } /** * Test that the password for an account can be successfully updated. */ public function test_password_is_updated(): void { /** @var User $user */ $user = User::factory()->create(); // Assign the user to two servers, one as the owner the other as a subuser, both // on different nodes to ensure our logic fires off correctly and the user has their // credentials revoked on both nodes. $server = $this->createServerModel(['owner_id' => $user->id]); $server2 = $this->createServerModel(); Subuser::factory()->for($server2)->for($user)->create(); $initialHash = $user->password; Bus::fake([RevokeSftpAccessJob::class]); $this->actingAs($user) ->putJson('/api/client/account/password', [ 'current_password' => 'password', 'password' => 'New_Password1', 'password_confirmation' => 'New_Password1', ]) ->assertNoContent(); $user = $user->refresh(); $this->assertNotEquals($user->password, $initialHash); $this->assertTrue(Hash::check('New_Password1', $user->password)); $this->assertFalse(Hash::check('password', $user->password)); $this->assertActivityFor('user:account.password-changed', $user, $user); $this->assertNotEquals($server->node_id, $server2->node_id); Bus::assertDispatchedTimes(RevokeSftpAccessJob::class, 2); Bus::assertDispatched(fn (RevokeSftpAccessJob $job) => $job->user === $user->uuid && $job->target->is($server->node)); Bus::assertDispatched(fn (RevokeSftpAccessJob $job) => $job->user === $user->uuid && $job->target->is($server2->node)); } /** * Test that the password for an account is not updated if the current password is not * provided correctly. */ public function test_password_is_not_updated_if_current_password_is_invalid(): void { /** @var User $user */ $user = User::factory()->create(); $response = $this->actingAs($user)->putJson('/api/client/account/password', [ 'current_password' => 'invalid', 'password' => 'New_Password1', 'password_confirmation' => 'New_Password1', ]); $response->assertStatus(Response::HTTP_BAD_REQUEST); $response->assertJsonPath('errors.0.code', 'InvalidPasswordProvidedException'); $response->assertJsonPath('errors.0.detail', 'The password provided was invalid for this account.'); } /** * Test that a validation error is returned to the user if no password is provided or if * the password is below the minimum password length. */ public function test_error_is_returned_for_invalid_request_data(): void { $user = User::factory()->create(); $this->actingAs($user)->putJson('/api/client/account/password', [ 'current_password' => 'password', ]) ->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY) ->assertJsonPath('errors.0.meta.rule', 'required'); $this->actingAs($user)->putJson('/api/client/account/password', [ 'current_password' => 'password', 'password' => 'pass', 'password_confirmation' => 'pass', ]) ->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY) ->assertJsonPath('errors.0.meta.rule', 'min'); } /** * Test that a validation error is returned if the password passed in the request * does not have a confirmation, or the confirmation is not the same as the password. */ public function test_error_is_returned_if_password_is_not_confirmed(): void { /** @var User $user */ $user = User::factory()->create(); $response = $this->actingAs($user)->putJson('/api/client/account/password', [ 'current_password' => 'password', 'password' => 'New_Password1', 'password_confirmation' => 'Invalid_New_Password', ]); $response->assertStatus(Response::HTTP_UNPROCESSABLE_ENTITY); $response->assertJsonPath('errors.0.meta.rule', 'confirmed'); $response->assertJsonPath('errors.0.detail', 'The password confirmation does not match.'); } }