asxpio/test/users_admin_test.rb
Sergei Poljanski 7e262b8f89
All checks were successful
Build and Deploy to Production / test (push) Successful in 27s
Build and Deploy to Production / build (push) Successful in 1m2s
Build and Deploy to Production / deploy (push) Successful in 40s
admin: fix the new-user username placeholder
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
2026-10-10 09:17:39 +04:00

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, '&quot;'
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