The username field's attributes were emitted as one string through <%= %>, which HTML-escapes the quotes, so the browser read the placeholder as the literal "leave and dropped the rest. Write the attributes in the template and test the rendered field. Assisted-by: Claude:opus-5
216 lines
9.1 KiB
Ruby
216 lines
9.1 KiB
Ruby
require_relative 'test_helper'
|
|
|
|
class UsersAdminTest < Minitest::Test
|
|
include Rack::Test::Methods
|
|
include AdminLogin
|
|
|
|
def app
|
|
AsxpioWeb
|
|
end
|
|
|
|
def setup
|
|
skip 'TEST_DATABASE_URL not set' unless TestDb.available?
|
|
TestDb.clean_users!
|
|
DB.connection[:audit_log].delete
|
|
Mail::TestMailer.deliveries.clear
|
|
end
|
|
|
|
def token(path = '/admin/users')
|
|
get path
|
|
last_response.body[/name="authenticity_token" value="([^"]+)"/, 1]
|
|
end
|
|
|
|
def create_user(params)
|
|
post '/admin/users', { authenticity_token: token('/admin/users/new') }.merge(params)
|
|
end
|
|
|
|
def invite_url
|
|
Mail::TestMailer.deliveries.last.body.to_s[%r{https?://\S+/admin/invite/\S+}]
|
|
end
|
|
|
|
def test_superuser_creates_user_and_invite_is_emailed_and_shown
|
|
admin_login!
|
|
create_user(username: 'carol', email: 'carol@example.com', perm: { 'bingo' => { 'read' => '1', 'delete' => '1' } })
|
|
assert_equal 302, last_response.status
|
|
carol = User[username: 'carol']
|
|
assert carol.can?('bingo', 'read')
|
|
assert carol.can?('bingo', 'delete')
|
|
refute carol.can?('bingo', 'write')
|
|
assert_nil carol.password_digest
|
|
assert_equal ['carol@example.com'], Mail::TestMailer.deliveries.last.to
|
|
assert_equal 'asxp.io admin', Mail::TestMailer.deliveries.last[:from].display_names.first
|
|
assert_equal ['test@example.com'], Mail::TestMailer.deliveries.last.from
|
|
get '/admin/users'
|
|
assert_includes last_response.body, 'emailed to carol@example.com'
|
|
assert_includes last_response.body, '/admin/invite/'
|
|
get '/admin/users'
|
|
refute_includes last_response.body, '/admin/invite/' # shown once
|
|
end
|
|
|
|
def test_invite_without_username_lets_invitee_choose
|
|
admin_login!
|
|
get '/admin/users/new'
|
|
assert_includes last_response.body, 'placeholder="empty: the invitee picks one"' # not HTML-escaped into the attribute
|
|
refute_includes last_response.body, '"'
|
|
create_user(username: '', perm: { 'bingo' => { 'read' => '1' } })
|
|
u = User.where(choose_username: true).first
|
|
assert_match(/\Ainvite-[0-9a-f]{8}\z/, u.username)
|
|
get '/admin/users'
|
|
link = last_response.body[%r{https?://\S+?/admin/invite/[\w-]+}]
|
|
assert link, 'invite link shown'
|
|
path = URI(link).path
|
|
clear_cookies
|
|
get path
|
|
assert_includes last_response.body, 'Choose a username'
|
|
tok = last_response.body[/name="authenticity_token" value="([^"]+)"/, 1]
|
|
post path, { authenticity_token: tok, username: ENV['ADMIN_USER'], password: 'kate-password-12', confirm: 'kate-password-12' }
|
|
assert_equal 422, last_response.status # taken
|
|
post path, { authenticity_token: tok, username: 'invite-x', password: 'kate-password-12', confirm: 'kate-password-12' }
|
|
assert_equal 422, last_response.status # reserved prefix
|
|
post path, { authenticity_token: tok, username: 'Kate', password: 'kate-password-12', confirm: 'kate-password-12' }
|
|
assert_equal 302, last_response.status
|
|
u.refresh
|
|
assert_equal 'kate', u.username
|
|
refute u.choose_username
|
|
assert u.can?('bingo', 'read')
|
|
end
|
|
|
|
def test_delete_implies_read
|
|
admin_login!
|
|
create_user(username: 'dave', perm: { 'bingo' => { 'delete' => '1' } })
|
|
assert User[username: 'dave'].can?('bingo', 'read')
|
|
end
|
|
|
|
def test_invite_accept_sets_password_and_is_single_use
|
|
admin_login!
|
|
create_user(username: 'carol', email: 'carol@example.com')
|
|
path = URI(invite_url).path
|
|
clear_cookies
|
|
get path
|
|
assert last_response.ok?
|
|
assert_includes last_response.body, 'carol'
|
|
tok = last_response.body[/name="authenticity_token" value="([^"]+)"/, 1]
|
|
post path, { authenticity_token: tok, password: 'short', confirm: 'short' }
|
|
assert_equal 422, last_response.status
|
|
post path, { authenticity_token: tok, password: 'carol-password-1', confirm: 'carol-password-1' }
|
|
assert_equal 302, last_response.status
|
|
assert User[username: 'carol'].authenticate('carol-password-1')
|
|
get '/admin'
|
|
assert last_response.ok? # logged in
|
|
clear_cookies
|
|
get path
|
|
assert_equal 404, last_response.status # used
|
|
end
|
|
|
|
def test_expired_invite_rejected
|
|
u = User.create(username: 'erin')
|
|
raw = UserToken.issue!(u, 'invite')
|
|
UserToken.where(user_id: u.id).update(expires_at: Time.now.utc - 1)
|
|
get "/admin/invite/#{raw}"
|
|
assert_equal 404, last_response.status
|
|
end
|
|
|
|
def test_reset_request_same_answer_and_mails_only_real_accounts
|
|
User.create(username: 'frank', email: 'frank@example.com', password: 'frank-password-1')
|
|
get '/admin/reset'
|
|
tok = last_response.body[/name="authenticity_token" value="([^"]+)"/, 1]
|
|
post '/admin/reset', { authenticity_token: tok, login: 'nobody' }
|
|
nobody = last_response.body
|
|
post '/admin/reset', { authenticity_token: tok, login: 'frank@example.com' }
|
|
assert_equal nobody.scan(/reset link is on its way/), last_response.body.scan(/reset link is on its way/)
|
|
assert_equal 1, Mail::TestMailer.deliveries.size
|
|
url = Mail::TestMailer.deliveries.last.body.to_s[%r{https?://\S+/admin/reset/\S+}]
|
|
get URI(url).path
|
|
assert last_response.ok?, "#{url.inspect} -> #{last_response.status} #{last_response.body[0, 200]}"
|
|
end
|
|
|
|
def test_no_escalation_by_user_manager
|
|
boss = User.create(username: 'manager', password: 'manager-pass-1')
|
|
UserPermission.create(user_id: boss.id, area: 'users', can_read: true, can_write: true)
|
|
UserPermission.create(user_id: boss.id, area: 'bingo', can_read: true)
|
|
target = User.create(username: 'grace', password: 'grace-password-1')
|
|
UserPermission.create(user_id: target.id, area: 'invoices', can_read: true)
|
|
admin_login!(username: 'manager', password: 'manager-pass-1')
|
|
|
|
create_user(username: 'henry', superuser: '1', perm: { 'invoices' => { 'write' => '1' }, 'bingo' => { 'read' => '1' } })
|
|
henry = User[username: 'henry']
|
|
refute henry.superuser
|
|
refute henry.can?('invoices', 'write') # manager lacks it
|
|
assert henry.can?('bingo', 'read')
|
|
|
|
post "/admin/users/#{target.id}", { authenticity_token: token, name: 'G', active: '1', perm: { 'bingo' => { 'read' => '1' } } }
|
|
target.refresh
|
|
assert target.can?('invoices', 'read') # kept: manager can't touch it
|
|
assert target.can?('bingo', 'read')
|
|
|
|
root = User.create(username: 'root', superuser: true, password: 'root-password-11')
|
|
post "/admin/users/#{root.id}", { authenticity_token: token, name: 'x', active: '' }
|
|
assert_equal 403, last_response.status
|
|
end
|
|
|
|
def test_cannot_delete_self_or_last_superuser
|
|
admin_login!
|
|
me = User[username: ENV['ADMIN_USER']]
|
|
post "/admin/users/#{me.id}/delete", { authenticity_token: token }
|
|
assert_equal 422, last_response.status
|
|
other = User.create(username: 'other', superuser: true, password: 'other-password-1')
|
|
post "/admin/users/#{other.id}/delete", { authenticity_token: token }
|
|
assert_equal 302, last_response.status
|
|
assert_nil User[other.id]
|
|
end
|
|
|
|
def test_disabling_logs_user_out
|
|
admin_login!
|
|
u = User.create(username: 'ivan', password: 'ivan-password-12')
|
|
v = u.session_version
|
|
post "/admin/users/#{u.id}", { authenticity_token: token, name: '', email: '' } # active unticked
|
|
u.refresh
|
|
refute u.active
|
|
assert_equal v + 1, u.session_version
|
|
end
|
|
|
|
def test_audit_log_records_and_lists
|
|
admin_login!
|
|
create_user(username: 'judy')
|
|
actions = DB.connection[:audit_log].select_map(:action)
|
|
assert_includes actions, 'auth.login'
|
|
assert_includes actions, 'user.create'
|
|
get '/admin/audit'
|
|
assert last_response.ok?
|
|
assert_includes last_response.body, 'user.create'
|
|
assert_includes last_response.body, 'judy'
|
|
end
|
|
|
|
def test_superuser_renames_including_self
|
|
admin_login!
|
|
me = User[username: ENV['ADMIN_USER']]
|
|
post "/admin/users/#{me.id}", { authenticity_token: token, username: 'Renamed', name: '' }
|
|
assert_equal 'renamed', me.refresh.username
|
|
get '/admin'
|
|
assert last_response.ok? # session survives
|
|
other = User.create(username: 'taken', password: 'taken-password-1')
|
|
post "/admin/users/#{me.id}", { authenticity_token: token, username: 'taken', name: '' }
|
|
assert_equal 'renamed', me.refresh.username # duplicate refused
|
|
assert_includes DB.connection[:audit_log].select_map(:action), 'user.rename'
|
|
end
|
|
|
|
def test_non_superuser_cannot_rename
|
|
boss = User.create(username: 'manager', password: 'manager-pass-1')
|
|
UserPermission.create(user_id: boss.id, area: 'users', can_read: true, can_write: true)
|
|
t = User.create(username: 'target', password: 'target-password1')
|
|
admin_login!(username: 'manager', password: 'manager-pass-1')
|
|
post "/admin/users/#{t.id}", { authenticity_token: token, username: 'hijacked', name: '', active: '1' }
|
|
assert_equal 'target', t.refresh.username
|
|
end
|
|
|
|
def test_users_read_only_cannot_create
|
|
u = User.create(username: 'viewer', password: 'viewer-password1')
|
|
UserPermission.create(user_id: u.id, area: 'users', can_read: true)
|
|
admin_login!(username: 'viewer', password: 'viewer-password1')
|
|
get '/admin/users'
|
|
assert last_response.ok?
|
|
refute_includes last_response.body, '+ New user'
|
|
get '/admin/users/new'
|
|
assert_equal 403, last_response.status
|
|
end
|
|
end
|