asxpio/test/admin_auth_test.rb
Sergei Poljanski 8471570187
admin: per-area permissions on every route
Give each user independent read, write and delete flags per area
(invoices, bingo, users), so someone can e.g. review and remove bingo
incidents without being able to add any. The request method and path
decide the flag needed: GET is read except the new/duplicate forms, POST
to .../delete is delete, any other POST is write. Unmapped admin paths
are superuser-only, and a test enumerates every registered admin route
so a new one cannot ship without a rule.

Pages follow the same flags: nav and dashboard tiles show readable areas
only, write forms and delete buttons are hidden, and the datacenter form
is shown read-only.

Assisted-by: Claude:opus-5
2026-10-10 08:27:37 +04:00

129 lines
4.7 KiB
Ruby

require_relative 'test_helper'
class AdminAuthTest < 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!
end
def login(username, password)
get '/admin/login'
token = last_response.body[/name="authenticity_token" value="([^"]+)"/, 1]
post '/admin/login', { authenticity_token: token, username: username, password: password }
end
def test_password_hashing
d = User.hash_password('correct horse')
assert_match(/\Ascrypt\$\d+\$8\$1\$/, d)
assert User.verify_password('correct horse', d)
refute User.verify_password('wrong', d)
refute_equal d, User.hash_password('correct horse') # salted
end
def test_bootstrap_only_once
User.bootstrap!('Boss', 'first-password-1')
assert User[username: 'boss'].superuser
User.bootstrap!('other', 'x' * 12)
assert_equal 1, User.count
end
def test_login_and_logout
User.create(username: 'alice', superuser: true, password: 'alice-password-1')
login('alice', 'alice-password-1')
assert_equal 302, last_response.status
get '/admin'
assert last_response.ok?
assert_includes last_response.body, 'alice'
get '/admin'
token = last_response.body[/name="authenticity_token" value="([^"]+)"/, 1]
post '/admin/logout', { authenticity_token: token }
get '/admin'
assert_equal 302, last_response.status
end
def test_wrong_password_and_unknown_user
User.create(username: 'alice', superuser: true, password: 'alice-password-1')
login('alice', 'nope')
assert_equal 401, last_response.status
login('mallory', 'alice-password-1')
assert_equal 401, last_response.status
end
def test_inactive_user_cannot_log_in
User.create(username: 'alice', superuser: true, active: false, password: 'alice-password-1')
User.create(username: 'root', superuser: true, password: 'root-password-11')
login('alice', 'alice-password-1')
assert_equal 401, last_response.status
end
def test_env_credentials_only_without_active_superuser
User.create(username: 'alice', superuser: true, password: 'alice-password-1')
login(ENV['ADMIN_USER'], ENV['ADMIN_PASSWORD'])
assert_equal 401, last_response.status # dormant: alice exists
User[username: 'alice'].update(active: false)
login(ENV['ADMIN_USER'], ENV['ADMIN_PASSWORD'])
assert_equal 302, last_response.status # recovery
assert User[username: ENV['ADMIN_USER']].superuser
end
def test_session_revoked_by_version_bump
u = User.create(username: 'alice', superuser: true, password: 'alice-password-1')
login('alice', 'alice-password-1')
get '/admin'
assert last_response.ok?
u.logout_everywhere!
get '/admin'
assert_equal 302, last_response.status
end
def test_user_without_permissions_limited_to_open_pages
User.create(username: 'bob', password: 'bob-password-12')
login('bob', 'bob-password-12')
get '/admin'
assert last_response.ok?
get '/admin/account/password'
assert last_response.ok?
get '/admin/invoices'
assert_equal 403, last_response.status
end
def test_must_change_password_flow
User.create(username: 'alice', superuser: true, must_change_password: true, password: 'temporary-pass-1')
login('alice', 'temporary-pass-1')
get '/admin/invoices'
assert_equal '/admin/account/password', URI(last_response.location).path
get '/admin/account/password'
token = last_response.body[/name="authenticity_token" value="([^"]+)"/, 1]
post '/admin/account/password', { authenticity_token: token, current: 'temporary-pass-1', password: 'short', confirm: 'short' }
assert_equal 422, last_response.status
post '/admin/account/password', { authenticity_token: token, current: 'temporary-pass-1',
password: 'brand-new-pass-1', confirm: 'brand-new-pass-1' }
assert_equal 302, last_response.status
refute User[username: 'alice'].must_change_password
assert User[username: 'alice'].authenticate('brand-new-pass-1')
end
def test_next_param_is_restricted_to_admin
User.create(username: 'alice', superuser: true, password: 'alice-password-1')
get '/admin/login?next=https://evil.example/'
token = last_response.body[/name="authenticity_token" value="([^"]+)"/, 1]
post '/admin/login', { authenticity_token: token, username: 'alice', password: 'alice-password-1', next: 'https://evil.example/' }
assert_equal '/admin', URI(last_response.location).path
end
def test_login_rate_limited
AdminAuth.login_limit = RateLimit.new(limit: 2, window: 900)
3.times { login('x', 'y') }
assert_equal 429, last_response.status
ensure
AdminAuth.login_limit = nil
end
end