19146: Add can_write/can_manage to users#list, fix select=can_*.
[arvados.git] / services / api / test / functional / arvados / v1 / users_controller_test.rb
index 3bcf3ed788dedc20097014e4cf3b0c59ccc49742..6a7b00a00573f9413a61856d66fd6e6eb3900c65 100644 (file)
@@ -13,11 +13,12 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     @initial_link_count = Link.count
     @vm_uuid = virtual_machines(:testvm).uuid
     ActionMailer::Base.deliveries = []
+    Rails.configuration.Users.ActivatedUsersAreVisibleToOthers = false
   end
 
   test "activate a user after signing UA" do
     authorize_with :inactive_but_signed_user_agreement
-    post :activate, id: users(:inactive_but_signed_user_agreement).uuid
+    post :activate, params: {id: users(:inactive_but_signed_user_agreement).uuid}
     assert_response :success
     assert_not_nil assigns(:object)
     me = JSON.parse(@response.body)
@@ -49,7 +50,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     authorize_with :inactive
     assert_equal false, users(:inactive).is_active
 
-    post :activate, id: users(:inactive).uuid
+    post :activate, params: {id: users(:inactive).uuid}
     assert_response 403
 
     resp = json_response
@@ -59,7 +60,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
 
   test "activate an already-active user" do
     authorize_with :active
-    post :activate, id: users(:active).uuid
+    post :activate, params: {id: users(:active).uuid}
     assert_response :success
     me = JSON.parse(@response.body)
     assert_equal true, me['is_active']
@@ -73,10 +74,12 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
 
   test "create new user with user as input" do
     authorize_with :admin
-    post :create, user: {
-      first_name: "test_first_name",
-      last_name: "test_last_name",
-      email: "foo@example.com"
+    post :create, params: {
+      user: {
+        first_name: "test_first_name",
+        last_name: "test_last_name",
+        email: "foo@example.com"
+      }
     }
     assert_response :success
     created = JSON.parse(@response.body)
@@ -86,13 +89,44 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     assert_nil created['identity_url'], 'expected no identity_url'
   end
 
+  test "create new user with empty username" do
+    authorize_with :admin
+    post :create, params: {
+      user: {
+        first_name: "test_first_name",
+        last_name: "test_last_name",
+        username: ""
+      }
+    }
+    assert_response :success
+    created = JSON.parse(@response.body)
+    assert_equal 'test_first_name', created['first_name']
+    assert_not_nil created['uuid'], 'expected uuid for the newly created user'
+    assert_nil created['email'], 'expected no email'
+    assert_nil created['username'], 'expected no username'
+  end
+
+  test "update user with empty username" do
+    authorize_with :admin
+    user = users('spectator')
+    assert_not_nil user['username']
+    put :update, params: {
+      id: users('spectator')['uuid'],
+      user: {
+        username: ""
+      }
+    }
+    assert_response :success
+    updated = JSON.parse(@response.body)
+    assert_nil updated['username'], 'expected no username'
+  end
+
   test "create user with user, vm and repo as input" do
     authorize_with :admin
     repo_name = 'usertestrepo'
 
-    post :setup, {
+    post :setup, params: {
       repo_name: repo_name,
-      openid_prefix: 'https://www.google.com/accounts/o8/id',
       user: {
         uuid: 'zzzzz-tpzed-abcdefghijklmno',
         first_name: "in_create_test_first_name",
@@ -111,11 +145,8 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     assert_not_nil created['email'], 'expected non-nil email'
     assert_nil created['identity_url'], 'expected no identity_url'
 
-    # arvados#user, repo link and link add user to 'All users' group
-    verify_links_added 4
-
-    verify_link response_items, 'arvados#user', true, 'permission', 'can_login',
-        created['uuid'], created['email'], 'arvados#user', false, 'User'
+    # repo link and link add user to 'All users' group
+    verify_links_added 3
 
     verify_link response_items, 'arvados#repository', true, 'permission', 'can_manage',
         "foo/#{repo_name}", created['uuid'], 'arvados#repository', true, 'Repository'
@@ -132,7 +163,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
   test "setup user with bogus uuid and expect error" do
     authorize_with :admin
 
-    post :setup, {
+    post :setup, params: {
       uuid: 'bogus_uuid',
       repo_name: 'usertestrepo',
       vm_uuid: @vm_uuid
@@ -146,11 +177,10 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
   test "setup user with bogus uuid in user and expect error" do
     authorize_with :admin
 
-    post :setup, {
+    post :setup, params: {
       user: {uuid: 'bogus_uuid'},
       repo_name: 'usertestrepo',
       vm_uuid: @vm_uuid,
-      openid_prefix: 'https://www.google.com/accounts/o8/id'
     }
     response_body = JSON.parse(@response.body)
     response_errors = response_body['errors']
@@ -162,10 +192,9 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
   test "setup user with no uuid and user, expect error" do
     authorize_with :admin
 
-    post :setup, {
+    post :setup, params: {
       repo_name: 'usertestrepo',
       vm_uuid: @vm_uuid,
-      openid_prefix: 'https://www.google.com/accounts/o8/id'
     }
     response_body = JSON.parse(@response.body)
     response_errors = response_body['errors']
@@ -177,11 +206,10 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
   test "setup user with no uuid and email, expect error" do
     authorize_with :admin
 
-    post :setup, {
+    post :setup, params: {
       user: {},
       repo_name: 'usertestrepo',
       vm_uuid: @vm_uuid,
-      openid_prefix: 'https://www.google.com/accounts/o8/id'
     }
     response_body = JSON.parse(@response.body)
     response_errors = response_body['errors']
@@ -194,7 +222,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     authorize_with :admin
     inactive_user = users(:inactive)
 
-    post :setup, {
+    post :setup, params: {
       uuid: users(:inactive).uuid,
       repo_name: 'usertestrepo',
       vm_uuid: @vm_uuid
@@ -222,7 +250,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     authorize_with :admin
     inactive_user = users(:inactive)
 
-    post :setup, {
+    post :setup, params: {
       uuid: inactive_user['uuid'],
       user: {email: 'junk_email'}
     }
@@ -241,10 +269,9 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
   test "setup user with valid email and repo as input" do
     authorize_with :admin
 
-    post :setup, {
+    post :setup, params: {
       repo_name: 'usertestrepo',
       user: {email: 'foo@example.com'},
-      openid_prefix: 'https://www.google.com/accounts/o8/id'
     }
 
     assert_response :success
@@ -253,18 +280,17 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     assert_not_nil response_object['uuid'], 'expected uuid for the new user'
     assert_equal response_object['email'], 'foo@example.com', 'expected given email'
 
-    # four extra links; system_group, login, group and repo perms
-    verify_links_added 4
+    # three extra links; system_group, group and repo perms
+    verify_links_added 3
   end
 
   test "setup user with fake vm and expect error" do
     authorize_with :admin
 
-    post :setup, {
+    post :setup, params: {
       repo_name: 'usertestrepo',
       vm_uuid: 'no_such_vm',
       user: {email: 'foo@example.com'},
-      openid_prefix: 'https://www.google.com/accounts/o8/id'
     }
 
     response_body = JSON.parse(@response.body)
@@ -277,9 +303,8 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
   test "setup user with valid email, repo and real vm as input" do
     authorize_with :admin
 
-    post :setup, {
+    post :setup, params: {
       repo_name: 'usertestrepo',
-      openid_prefix: 'https://www.google.com/accounts/o8/id',
       vm_uuid: @vm_uuid,
       user: {email: 'foo@example.com'}
     }
@@ -290,16 +315,15 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     assert_not_nil response_object['uuid'], 'expected uuid for the new user'
     assert_equal response_object['email'], 'foo@example.com', 'expected given email'
 
-    # five extra links; system_group, login, group, vm, repo
-    verify_links_added 5
+    # four extra links; system_group, group, vm, repo
+    verify_links_added 4
   end
 
   test "setup user with valid email, no vm and no repo as input" do
     authorize_with :admin
 
-    post :setup, {
+    post :setup, params: {
       user: {email: 'foo@example.com'},
-      openid_prefix: 'https://www.google.com/accounts/o8/id'
     }
 
     assert_response :success
@@ -308,11 +332,8 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     assert_not_nil response_object['uuid'], 'expected uuid for new user'
     assert_equal response_object['email'], 'foo@example.com', 'expected given email'
 
-    # three extra links; system_group, login, and group
-    verify_links_added 3
-
-    verify_link response_items, 'arvados#user', true, 'permission', 'can_login',
-        response_object['uuid'], response_object['email'], 'arvados#user', false, 'User'
+    # two extra links; system_group, and group
+    verify_links_added 2
 
     verify_link response_items, 'arvados#group', true, 'permission', 'can_read',
         'All users', response_object['uuid'], 'arvados#group', true, 'Group'
@@ -327,8 +348,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
   test "setup user with email, first name, repo name and vm uuid" do
     authorize_with :admin
 
-    post :setup, {
-      openid_prefix: 'https://www.google.com/accounts/o8/id',
+    post :setup, params: {
       repo_name: 'usertestrepo',
       vm_uuid: @vm_uuid,
       user: {
@@ -345,16 +365,15 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     assert_equal 'test_first_name', response_object['first_name'],
         'expecting first name'
 
-    # five extra links; system_group, login, group, repo and vm
-    verify_links_added 5
+    # four extra links; system_group, group, repo and vm
+    verify_links_added 4
   end
 
   test "setup user with an existing user email and check different object is created" do
     authorize_with :admin
     inactive_user = users(:inactive)
 
-    post :setup, {
-      openid_prefix: 'https://www.google.com/accounts/o8/id',
+    post :setup, params: {
       repo_name: 'usertestrepo',
       user: {
         email: inactive_user['email']
@@ -368,16 +387,15 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     assert_not_equal response_object['uuid'], inactive_user['uuid'],
         'expected different uuid after create operation'
     assert_equal inactive_user['email'], response_object['email'], 'expected given email'
-    # system_group, openid, group, and repo. No vm link.
-    verify_links_added 4
+    # system_group, group, and repo. No vm link.
+    verify_links_added 3
   end
 
   test "setup user with openid prefix" do
     authorize_with :admin
 
-    post :setup, {
+    post :setup, params: {
       repo_name: 'usertestrepo',
-      openid_prefix: 'http://www.example.com/account',
       user: {
         first_name: "in_create_test_first_name",
         last_name: "test_last_name",
@@ -396,11 +414,8 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     assert_nil created['identity_url'], 'expected no identity_url'
 
     # verify links
-    # four new links: system_group, arvados#user, repo, and 'All users' group.
-    verify_links_added 4
-
-    verify_link response_items, 'arvados#user', true, 'permission', 'can_login',
-        created['uuid'], created['email'], 'arvados#user', false, 'User'
+    # three new links: system_group, repo, and 'All users' group.
+    verify_links_added 3
 
     verify_link response_items, 'arvados#repository', true, 'permission', 'can_manage',
         'foo/usertestrepo', created['uuid'], 'arvados#repository', true, 'Repository'
@@ -412,29 +427,10 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
         nil, created['uuid'], 'arvados#virtualMachine', false, 'VirtualMachine'
   end
 
-  test "invoke setup with no openid prefix, expect error" do
-    authorize_with :admin
-
-    post :setup, {
-      repo_name: 'usertestrepo',
-      user: {
-        first_name: "in_create_test_first_name",
-        last_name: "test_last_name",
-        email: "foo@example.com"
-      }
-    }
-
-    response_body = JSON.parse(@response.body)
-    response_errors = response_body['errors']
-    assert_not_nil response_errors, 'Expected error in response'
-    assert (response_errors.first.include? 'openid_prefix parameter is missing'),
-        'Expected ArgumentError'
-  end
-
   test "setup user with user, vm and repo and verify links" do
     authorize_with :admin
 
-    post :setup, {
+    post :setup, params: {
       user: {
         first_name: "in_create_test_first_name",
         last_name: "test_last_name",
@@ -442,7 +438,6 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
       },
       vm_uuid: @vm_uuid,
       repo_name: 'usertestrepo',
-      openid_prefix: 'https://www.google.com/accounts/o8/id'
     }
 
     assert_response :success
@@ -455,12 +450,10 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     assert_not_nil created['email'], 'expected non-nil email'
     assert_nil created['identity_url'], 'expected no identity_url'
 
-    # five new links: system_group, arvados#user, repo, vm and 'All
-    # users' group link
-    verify_links_added 5
+    # four new links: system_group, repo, vm and 'All users' group link
+    verify_links_added 4
 
-    verify_link response_items, 'arvados#user', true, 'permission', 'can_login',
-        created['uuid'], created['email'], 'arvados#user', false, 'User'
+    # system_group isn't part of the response.  See User#add_system_group_permission_link
 
     verify_link response_items, 'arvados#repository', true, 'permission', 'can_manage',
         'foo/usertestrepo', created['uuid'], 'arvados#repository', true, 'Repository'
@@ -475,7 +468,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
   test "create user as non admin user and expect error" do
     authorize_with :active
 
-    post :create, {
+    post :create, params: {
       user: {email: 'foo@example.com'}
     }
 
@@ -489,8 +482,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
   test "setup user as non admin user and expect error" do
     authorize_with :active
 
-    post :setup, {
-      openid_prefix: 'https://www.google.com/accounts/o8/id',
+    post :setup, params: {
       user: {email: 'foo@example.com'}
     }
 
@@ -506,7 +498,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     active_user = users(:active)
 
     # invoke setup with a repository
-    post :setup, {
+    post :setup, params: {
       repo_name: 'usertestrepo',
       uuid: active_user['uuid']
     }
@@ -539,7 +531,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     repo_link_count = repo_link_query.count
 
     # invoke setup with a repository
-    post :setup, {
+    post :setup, params: {
       vm_uuid: @vm_uuid,
       uuid: active_user['uuid'],
       email: 'junk_email'
@@ -574,7 +566,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     authorize_with :admin
 
     # now unsetup this user
-    post :unsetup, id: active_user['uuid']
+    post :unsetup, params: {id: active_user['uuid']}
     assert_response :success
 
     response_user = JSON.parse(@response.body)
@@ -598,8 +590,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
   test "setup user with send notification param false and verify no email" do
     authorize_with :admin
 
-    post :setup, {
-      openid_prefix: 'http://www.example.com/account',
+    post :setup, params: {
       send_notification_email: 'false',
       user: {
         email: "foo@example.com"
@@ -619,8 +610,24 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
   test "setup user with send notification param true and verify email" do
     authorize_with :admin
 
-    post :setup, {
-      openid_prefix: 'http://www.example.com/account',
+    Rails.configuration.Users.UserSetupMailText = %{
+<% if not @user.full_name.empty? -%>
+<%= @user.full_name %>,
+<% else -%>
+Hi there,
+<% end -%>
+
+Your Arvados shell account has been set up. Please visit the virtual machines page <% if Rails.configuration.Services.Workbench1.ExternalURL %>at
+
+<%= Rails.configuration.Services.Workbench1.ExternalURL %><%= "/" if !Rails.configuration.Services.Workbench1.ExternalURL.to_s.end_with?("/") %>users/<%= @user.uuid%>/virtual_machines <% else %><% end %>
+
+for connection instructions.
+
+Thanks,
+The Arvados team.
+}
+
+    post :setup, params: {
       send_notification_email: 'true',
       user: {
         email: "foo@example.com"
@@ -636,12 +643,12 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     setup_email = ActionMailer::Base.deliveries.last
     assert_not_nil setup_email, 'Expected email after setup'
 
-    assert_equal Rails.configuration.user_notifier_email_from, setup_email.from[0]
+    assert_equal Rails.configuration.Users.UserNotifierEmailFrom, setup_email.from[0]
     assert_equal 'foo@example.com', setup_email.to[0]
-    assert_equal 'Welcome to Curoverse - shell account enabled', setup_email.subject
+    assert_equal 'Welcome to Arvados - account enabled', setup_email.subject
     assert (setup_email.body.to_s.include? 'Your Arvados shell account has been set up'),
         'Expected Your Arvados shell account has been set up in email body'
-    assert (setup_email.body.to_s.include? "#{Rails.configuration.workbench_address}users/#{created['uuid']}/virtual_machines"), 'Expected virtual machines url in email body'
+    assert (setup_email.body.to_s.include? "#{Rails.configuration.Services.Workbench1.ExternalURL}users/#{created['uuid']}/virtual_machines"), 'Expected virtual machines url in email body'
   end
 
   test "setup inactive user by changing is_active to true" do
@@ -649,7 +656,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     active_user = users(:active)
 
     # invoke setup with a repository
-    put :update, {
+    put :update, params: {
           id: active_user['uuid'],
           user: {
             is_active: true,
@@ -667,29 +674,35 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     get(:index)
     check_non_admin_index
     check_readable_users_index [:spectator], [:inactive, :active]
+    json_response["items"].each do |u|
+      if u["uuid"] == users(:spectator).uuid
+        assert_equal true, u["can_write"]
+        assert_equal true, u["can_manage"]
+      end
+    end
   end
 
   test "non-admin user gets only safe attributes from users#show" do
     g = act_as_system_user do
-      create :group
+      create :group, group_class: "role"
     end
     users = create_list :active_user, 2, join_groups: [g]
     token = create :token, user: users[0]
     authorize_with_token token
-    get :show, id: users[1].uuid
+    get :show, params: {id: users[1].uuid}
     check_non_admin_show
   end
 
   [2, 4].each do |limit|
     test "non-admin user can limit index to #{limit}" do
       g = act_as_system_user do
-        create :group
+        create :group, group_class: "role"
       end
       users = create_list :active_user, 4, join_groups: [g]
       token = create :token, user: users[0]
 
       authorize_with_token token
-      get(:index, limit: limit)
+      get(:index, params: {limit: limit})
       check_non_admin_index
       assert_equal(limit, json_response["items"].size,
                    "non-admin index limit was ineffective")
@@ -708,14 +721,14 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
 
   test "admin can filter on user.is_active" do
     authorize_with :admin
-    get(:index, filters: [["is_active", "=", "true"]])
+    get(:index, params: {filters: [["is_active", "=", "true"]]})
     assert_response :success
     check_readable_users_index [:active, :spectator], [:inactive]
   end
 
   test "admin can search where user.is_active" do
     authorize_with :admin
-    get(:index, where: {is_active: true})
+    get(:index, params: {where: {is_active: true}})
     assert_response :success
     check_readable_users_index [:active, :spectator], [:inactive]
   end
@@ -723,7 +736,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
   test "update active_no_prefs user profile and expect notification email" do
     authorize_with :admin
 
-    put :update, {
+    put :update, params: {
       id: users(:active_no_prefs).uuid,
       user: {
         prefs: {:profile => {'organization' => 'example.com'}}
@@ -747,7 +760,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     user = {}
     user[:prefs] = users(:active_no_prefs_profile_no_getting_started_shown).prefs
     user[:prefs][:profile] = {:profile => {'organization' => 'example.com'}}
-    put :update, {
+    put :update, params: {
       id: users(:active_no_prefs_profile_no_getting_started_shown).uuid,
       user: user
     }
@@ -766,7 +779,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
   test "update active user profile and expect no notification email" do
     authorize_with :admin
 
-    put :update, {
+    put :update, params: {
       id: users(:active).uuid,
       user: {
         prefs: {:profile => {'organization' => 'anotherexample.com'}}
@@ -794,40 +807,51 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
                     "user's writable_by should include its owner_uuid")
   end
 
-  [
-    [:admin, true],
-    [:active, false],
-  ].each do |auth_user, expect_success|
-    test "update_uuid as #{auth_user}" do
-      authorize_with auth_user
-      orig_uuid = users(:active).uuid
-      post :update_uuid, {
-             id: orig_uuid,
-             new_uuid: 'zbbbb-tpzed-abcde12345abcde',
-           }
-      if expect_success
-        assert_response :success
-        assert_empty User.where(uuid: orig_uuid)
-      else
-        assert_response 403
-        assert_not_empty User.where(uuid: orig_uuid)
-      end
-    end
-  end
-
-  test "refuse to merge with redirect_to_user_uuid=false (not yet supported)" do
+  test "merge with redirect_to_user_uuid=false" do
     authorize_with :project_viewer_trustedclient
-    post :merge, {
+    tok = api_client_authorizations(:project_viewer).api_token
+    post :merge, params: {
            new_user_token: api_client_authorizations(:active_trustedclient).api_token,
            new_owner_uuid: users(:active).uuid,
            redirect_to_new_user: false,
          }
-    assert_response(422)
+    assert_response(:success)
+    assert_nil(User.unscoped.find_by_uuid(users(:project_viewer).uuid).redirect_to_user_uuid)
+
+    # because redirect_to_new_user=false, token owned by
+    # project_viewer should be deleted
+    auth = ApiClientAuthorization.validate(token: tok)
+    assert_nil(auth)
+  end
+
+  test "merge remote to local as admin" do
+    authorize_with :admin
+
+    remoteuser = User.create!(uuid: "zbbbb-tpzed-remotremotremot")
+    tok = ApiClientAuthorization.create!(user: remoteuser, api_client: api_clients(:untrusted)).api_token
+
+    auth = ApiClientAuthorization.validate(token: tok)
+    assert_not_nil(auth)
+    assert_nil(remoteuser.redirect_to_user_uuid)
+
+    post :merge, params: {
+           new_user_uuid: users(:active).uuid,
+           old_user_uuid: remoteuser.uuid,
+           new_owner_uuid: users(:active).uuid,
+           redirect_to_new_user: true,
+         }
+    assert_response(:success)
+    remoteuser.reload
+    assert_equal(users(:active).uuid, remoteuser.redirect_to_user_uuid)
+
+    # token owned by remoteuser should be deleted
+    auth = ApiClientAuthorization.validate(token: tok)
+    assert_nil(auth)
   end
 
   test "refuse to merge user into self" do
     authorize_with(:active_trustedclient)
-    post(:merge, {
+    post(:merge, params: {
            new_user_token: api_client_authorizations(:active_trustedclient).api_token,
            new_owner_uuid: users(:active).uuid,
            redirect_to_new_user: true,
@@ -839,7 +863,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
    [:active_trustedclient, :project_viewer]].each do |src, dst|
     test "refuse to merge with untrusted token (#{src} -> #{dst})" do
       authorize_with(src)
-      post(:merge, {
+      post(:merge, params: {
              new_user_token: api_client_authorizations(dst).api_token,
              new_owner_uuid: api_client_authorizations(dst).user.uuid,
              redirect_to_new_user: true,
@@ -852,7 +876,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
    [:project_viewer_trustedclient, :expired_trustedclient]].each do |src, dst|
     test "refuse to merge with expired token (#{src} -> #{dst})" do
       authorize_with(src)
-      post(:merge, {
+      post(:merge, params: {
              new_user_token: api_client_authorizations(dst).api_token,
              new_owner_uuid: api_client_authorizations(dst).user.uuid,
              redirect_to_new_user: true,
@@ -868,7 +892,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
         api_client_authorizations(auth).update_attributes(scopes: ["GET /", "POST /", "PUT /"])
       end
       authorize_with(:active_trustedclient)
-      post(:merge, {
+      post(:merge, params: {
              new_user_token: api_client_authorizations(:project_viewer_trustedclient).api_token,
              new_owner_uuid: users(:project_viewer).uuid,
              redirect_to_new_user: true,
@@ -879,7 +903,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
 
   test "refuse to merge if new_owner_uuid is not writable" do
     authorize_with(:project_viewer_trustedclient)
-    post(:merge, {
+    post(:merge, params: {
            new_user_token: api_client_authorizations(:active_trustedclient).api_token,
            new_owner_uuid: groups(:anonymously_accessible_project).uuid,
            redirect_to_new_user: true,
@@ -887,9 +911,28 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     assert_response(403)
   end
 
+  test "refuse to merge if new_owner_uuid is empty" do
+    authorize_with(:project_viewer_trustedclient)
+    post(:merge, params: {
+           new_user_token: api_client_authorizations(:active_trustedclient).api_token,
+           new_owner_uuid: "",
+           redirect_to_new_user: true,
+         })
+    assert_response(422)
+  end
+
+  test "refuse to merge if new_owner_uuid is not provided" do
+    authorize_with(:project_viewer_trustedclient)
+    post(:merge, params: {
+           new_user_token: api_client_authorizations(:active_trustedclient).api_token,
+           redirect_to_new_user: true,
+         })
+    assert_response(422)
+  end
+
   test "refuse to update redirect_to_user_uuid directly" do
     authorize_with(:active_trustedclient)
-    patch(:update, {
+    patch(:update, params: {
             id: users(:active).uuid,
             user: {
               redirect_to_user_uuid: users(:active).uuid,
@@ -900,13 +943,31 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
 
   test "merge 'project_viewer' account into 'active' account" do
     authorize_with(:project_viewer_trustedclient)
-    post(:merge, {
+    post(:merge, params: {
            new_user_token: api_client_authorizations(:active_trustedclient).api_token,
            new_owner_uuid: users(:active).uuid,
            redirect_to_new_user: true,
          })
     assert_response(:success)
-    assert_equal(users(:project_viewer).redirect_to_user_uuid, users(:active).uuid)
+    assert_equal(users(:active).uuid, User.unscoped.find_by_uuid(users(:project_viewer).uuid).redirect_to_user_uuid)
+
+    auth = ApiClientAuthorization.validate(token: api_client_authorizations(:project_viewer).api_token)
+    assert_not_nil(auth)
+    assert_not_nil(auth.user)
+    assert_equal(users(:active).uuid, auth.user.uuid)
+  end
+
+
+  test "merge 'project_viewer' account into 'active' account using uuids" do
+    authorize_with(:admin)
+    post(:merge, params: {
+           old_user_uuid: users(:project_viewer).uuid,
+           new_user_uuid: users(:active).uuid,
+           new_owner_uuid: users(:active).uuid,
+           redirect_to_new_user: true,
+         })
+    assert_response(:success)
+    assert_equal(users(:active).uuid, User.unscoped.find_by_uuid(users(:project_viewer).uuid).redirect_to_user_uuid)
 
     auth = ApiClientAuthorization.validate(token: api_client_authorizations(:project_viewer).api_token)
     assert_not_nil(auth)
@@ -914,8 +975,117 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
     assert_equal(users(:active).uuid, auth.user.uuid)
   end
 
+  test "merge 'project_viewer' account into 'active' account using uuids denied for non-admin" do
+    authorize_with(:active)
+    post(:merge, params: {
+           old_user_uuid: users(:project_viewer).uuid,
+           new_user_uuid: users(:active).uuid,
+           new_owner_uuid: users(:active).uuid,
+           redirect_to_new_user: true,
+         })
+    assert_response(403)
+    assert_nil(users(:project_viewer).redirect_to_user_uuid)
+  end
+
+  test "merge 'project_viewer' account into 'active' account using uuids denied missing old_user_uuid" do
+    authorize_with(:admin)
+    post(:merge, params: {
+           new_user_uuid: users(:active).uuid,
+           new_owner_uuid: users(:active).uuid,
+           redirect_to_new_user: true,
+         })
+    assert_response(422)
+    assert_nil(users(:project_viewer).redirect_to_user_uuid)
+  end
+
+  test "merge 'project_viewer' account into 'active' account using uuids denied missing new_user_uuid" do
+    authorize_with(:admin)
+    post(:merge, params: {
+           old_user_uuid: users(:project_viewer).uuid,
+           new_owner_uuid: users(:active).uuid,
+           redirect_to_new_user: true,
+         })
+    assert_response(422)
+    assert_nil(users(:project_viewer).redirect_to_user_uuid)
+  end
+
+  test "merge 'project_viewer' account into 'active' account using uuids denied bogus old_user_uuid" do
+    authorize_with(:admin)
+    post(:merge, params: {
+           old_user_uuid: "zzzzz-tpzed-bogusbogusbogus",
+           new_user_uuid: users(:active).uuid,
+           new_owner_uuid: users(:active).uuid,
+           redirect_to_new_user: true,
+         })
+    assert_response(422)
+    assert_nil(users(:project_viewer).redirect_to_user_uuid)
+  end
+
+  test "merge 'project_viewer' account into 'active' account using uuids denied bogus new_user_uuid" do
+    authorize_with(:admin)
+    post(:merge, params: {
+           old_user_uuid: users(:project_viewer).uuid,
+           new_user_uuid: "zzzzz-tpzed-bogusbogusbogus",
+           new_owner_uuid: users(:active).uuid,
+           redirect_to_new_user: true,
+         })
+    assert_response(422)
+    assert_nil(users(:project_viewer).redirect_to_user_uuid)
+  end
+
+  test "batch update fails for non-admin" do
+    authorize_with(:active)
+    patch(:batch_update, params: {updates: {}})
+    assert_response(403)
+  end
+
+  test "batch update" do
+    existinguuid = 'remot-tpzed-foobarbazwazqux'
+    newuuid = 'remot-tpzed-newnarnazwazqux'
+    unchanginguuid = 'remot-tpzed-nochangingattrs'
+    act_as_system_user do
+      User.create!(uuid: existinguuid, email: 'root@existing.example.com')
+      User.create!(uuid: unchanginguuid, email: 'root@unchanging.example.com', prefs: {'foo' => {'bar' => 'baz'}})
+    end
+    assert_equal(1, Log.where(object_uuid: unchanginguuid).count)
+
+    authorize_with(:admin)
+    patch(:batch_update,
+          params: {
+            updates: {
+              existinguuid => {
+                'first_name' => 'root',
+                'email' => 'root@remot.example.com',
+                'is_active' => true,
+                'is_admin' => true,
+                'prefs' => {'foo' => 'bar'},
+              },
+              newuuid => {
+                'first_name' => 'noot',
+                'email' => 'root@remot.example.com',
+                'username' => '',
+              },
+              unchanginguuid => {
+                'email' => 'root@unchanging.example.com',
+                'prefs' => {'foo' => {'bar' => 'baz'}},
+              },
+            }})
+    assert_response(:success)
+
+    assert_equal('root', User.find_by_uuid(existinguuid).first_name)
+    assert_equal('root@remot.example.com', User.find_by_uuid(existinguuid).email)
+    assert_equal(true, User.find_by_uuid(existinguuid).is_active)
+    assert_equal(true, User.find_by_uuid(existinguuid).is_admin)
+    assert_equal({'foo' => 'bar'}, User.find_by_uuid(existinguuid).prefs)
+
+    assert_equal('noot', User.find_by_uuid(newuuid).first_name)
+    assert_equal('root@remot.example.com', User.find_by_uuid(newuuid).email)
+
+    assert_equal(1, Log.where(object_uuid: unchanginguuid).count)
+  end
+
   NON_ADMIN_USER_DATA = ["uuid", "kind", "is_active", "email", "first_name",
-                         "last_name", "username"].sort
+                         "last_name", "username", "can_write", "can_manage"].sort
 
   def check_non_admin_index
     assert_response :success
@@ -953,7 +1123,7 @@ class Arvados::V1::UsersControllerTest < ActionController::TestCase
 
   def check_inactive_user_findable(params={})
     inactive_user = users(:inactive)
-    get(:index, params.merge(filters: [["email", "=", inactive_user.email]]))
+    get(:index, params: params.merge(filters: [["email", "=", inactive_user.email]]))
     assert_response :success
     user_list = json_response["items"]
     assert_equal(1, user_list.andand.count)