- Switch from SolidQueue to async job processor for simpler background job handling - Remove SolidQueue gem and related configuration files - Add letter_opener gem for development email preview - Fix invitation email template issues (invitation_login_token method and route helper) - Configure SMTP settings via environment variables in application.rb - Add email delivery configuration banner on admin users page - Improve admin users page with inline action buttons and SMTP configuration warnings - Update development and production environments to use async processor - Add helper methods to detect SMTP configuration and filter out localhost settings 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
396 lines
12 KiB
Ruby
396 lines
12 KiB
Ruby
require "test_helper"
|
|
|
|
class ForwardAuthRuleTest < ActiveSupport::TestCase
|
|
def setup
|
|
@rule = ForwardAuthRule.new(
|
|
domain_pattern: "*.example.com",
|
|
active: true
|
|
)
|
|
end
|
|
|
|
test "should be valid with valid attributes" do
|
|
assert @rule.valid?
|
|
end
|
|
|
|
test "should require domain_pattern" do
|
|
@rule.domain_pattern = ""
|
|
assert_not @rule.valid?
|
|
assert_includes @rule.errors[:domain_pattern], "can't be blank"
|
|
end
|
|
|
|
test "should require active to be boolean" do
|
|
@rule.active = nil
|
|
assert_not @rule.valid?
|
|
assert_includes @rule.errors[:active], "is not included in the list"
|
|
end
|
|
|
|
test "should normalize domain_pattern to lowercase" do
|
|
@rule.domain_pattern = "*.EXAMPLE.COM"
|
|
@rule.save!
|
|
assert_equal "*.example.com", @rule.reload.domain_pattern
|
|
end
|
|
|
|
test "should enforce unique domain_pattern" do
|
|
@rule.save!
|
|
duplicate = ForwardAuthRule.new(
|
|
domain_pattern: "*.example.com",
|
|
active: true
|
|
)
|
|
assert_not duplicate.valid?
|
|
assert_includes duplicate.errors[:domain_pattern], "has already been taken"
|
|
end
|
|
|
|
test "should match domain patterns correctly" do
|
|
@rule.save!
|
|
|
|
assert @rule.matches_domain?("app.example.com")
|
|
assert @rule.matches_domain?("api.example.com")
|
|
assert @rule.matches_domain?("sub.app.example.com")
|
|
assert_not @rule.matches_domain?("example.org")
|
|
assert_not @rule.matches_domain?("otherexample.com")
|
|
end
|
|
|
|
test "should handle exact domain matches" do
|
|
@rule.domain_pattern = "api.example.com"
|
|
@rule.save!
|
|
|
|
assert @rule.matches_domain?("api.example.com")
|
|
assert_not @rule.matches_domain?("app.example.com")
|
|
assert_not @rule.matches_domain?("sub.api.example.com")
|
|
end
|
|
|
|
test "policy_for_user should return bypass when no groups assigned" do
|
|
user = users(:one)
|
|
@rule.save!
|
|
|
|
assert_equal "bypass", @rule.policy_for_user(user)
|
|
end
|
|
|
|
test "policy_for_user should return deny for inactive rule" do
|
|
user = users(:one)
|
|
@rule.active = false
|
|
@rule.save!
|
|
|
|
assert_equal "deny", @rule.policy_for_user(user)
|
|
end
|
|
|
|
test "policy_for_user should return deny for inactive user" do
|
|
user = users(:one)
|
|
user.update!(active: false)
|
|
@rule.save!
|
|
|
|
assert_equal "deny", @rule.policy_for_user(user)
|
|
end
|
|
|
|
test "policy_for_user should return correct policy based on user groups and TOTP" do
|
|
group = groups(:one)
|
|
user_with_totp = users(:two)
|
|
user_without_totp = users(:one)
|
|
|
|
user_with_totp.totp_secret = "test_secret"
|
|
user_with_totp.save!
|
|
|
|
@rule.allowed_groups << group
|
|
user_with_totp.groups << group
|
|
user_without_totp.groups << group
|
|
@rule.save!
|
|
|
|
assert_equal "two_factor", @rule.policy_for_user(user_with_totp)
|
|
assert_equal "one_factor", @rule.policy_for_user(user_without_totp)
|
|
end
|
|
|
|
test "user_allowed? should return true when no groups assigned" do
|
|
user = users(:one)
|
|
@rule.save!
|
|
|
|
assert @rule.user_allowed?(user)
|
|
end
|
|
|
|
test "user_allowed? should return true when user in allowed groups" do
|
|
group = groups(:one)
|
|
user = users(:one)
|
|
user.groups << group
|
|
@rule.allowed_groups << group
|
|
@rule.save!
|
|
|
|
assert @rule.user_allowed?(user)
|
|
end
|
|
|
|
test "user_allowed? should return false when user not in allowed groups" do
|
|
group = groups(:one)
|
|
user = users(:one)
|
|
@rule.allowed_groups << group
|
|
@rule.save!
|
|
|
|
assert_not @rule.user_allowed?(user)
|
|
end
|
|
|
|
# Header Configuration Tests
|
|
test "effective_headers should return default headers when no custom config" do
|
|
@rule.save!
|
|
|
|
expected = ForwardAuthRule::DEFAULT_HEADERS
|
|
assert_equal expected, @rule.effective_headers
|
|
end
|
|
|
|
test "effective_headers should merge custom headers with defaults" do
|
|
@rule.save!
|
|
@rule.update!(headers_config: { user: "X-Forwarded-User", email: "X-Forwarded-Email" })
|
|
|
|
expected = ForwardAuthRule::DEFAULT_HEADERS.merge(
|
|
user: "X-Forwarded-User",
|
|
email: "X-Forwarded-Email"
|
|
)
|
|
assert_equal expected, @rule.effective_headers
|
|
end
|
|
|
|
test "headers_for_user should generate correct headers for user with groups" do
|
|
group = groups(:one)
|
|
user = users(:one)
|
|
user.groups << group
|
|
@rule.save!
|
|
|
|
headers = @rule.headers_for_user(user)
|
|
|
|
assert_equal user.email_address, headers["X-Remote-User"]
|
|
assert_equal user.email_address, headers["X-Remote-Email"]
|
|
assert_equal user.email_address, headers["X-Remote-Name"]
|
|
assert_equal group.name, headers["X-Remote-Groups"]
|
|
assert_equal "true", headers["X-Remote-Admin"]
|
|
end
|
|
|
|
test "headers_for_user should generate correct headers for user without groups" do
|
|
user = users(:one)
|
|
@rule.save!
|
|
|
|
headers = @rule.headers_for_user(user)
|
|
|
|
assert_equal user.email_address, headers["X-Remote-User"]
|
|
assert_equal user.email_address, headers["X-Remote-Email"]
|
|
assert_equal user.email_address, headers["X-Remote-Name"]
|
|
assert_nil headers["X-Remote-Groups"] # No groups, no header
|
|
assert_equal "true", headers["X-Remote-Admin"]
|
|
end
|
|
|
|
test "headers_for_user should work with custom headers" do
|
|
user = users(:one)
|
|
@rule.update!(headers_config: {
|
|
user: "X-Forwarded-User",
|
|
groups: "X-Custom-Groups"
|
|
})
|
|
|
|
headers = @rule.headers_for_user(user)
|
|
|
|
assert_equal user.email_address, headers["X-Forwarded-User"]
|
|
assert_nil headers["X-Remote-User"] # Should be overridden
|
|
assert_equal user.email_address, headers["X-Remote-Email"] # Default preserved
|
|
assert_nil headers["X-Custom-Groups"] # User has no groups
|
|
end
|
|
|
|
test "headers_for_user should return empty hash when all headers disabled" do
|
|
user = users(:one)
|
|
@rule.update!(headers_config: {
|
|
user: "",
|
|
email: "",
|
|
name: "",
|
|
groups: "",
|
|
admin: ""
|
|
})
|
|
|
|
headers = @rule.headers_for_user(user)
|
|
assert_empty headers
|
|
end
|
|
|
|
test "headers_disabled? should correctly identify disabled headers" do
|
|
@rule.save!
|
|
assert_not @rule.headers_disabled?
|
|
|
|
@rule.update!(headers_config: { user: "X-Custom-User" })
|
|
assert_not @rule.headers_disabled?
|
|
|
|
@rule.update!(headers_config: { user: "", email: "", name: "", groups: "", admin: "" })
|
|
assert @rule.headers_disabled?
|
|
end
|
|
|
|
# Additional Domain Pattern Tests
|
|
test "matches_domain? should handle complex patterns" do
|
|
@rule.save!
|
|
|
|
# Test multiple wildcards
|
|
@rule.update!(domain_pattern: "*.*.example.com")
|
|
assert @rule.matches_domain?("app.dev.example.com")
|
|
assert @rule.matches_domain?("api.staging.example.com")
|
|
assert_not @rule.matches_domain?("example.com")
|
|
assert_not @rule.matches_domain?("app.example.org")
|
|
|
|
# Test exact domain with dots
|
|
@rule.update!(domain_pattern: "api.v2.example.com")
|
|
assert @rule.matches_domain?("api.v2.example.com")
|
|
assert_not @rule.matches_domain?("api.v3.example.com")
|
|
assert_not @rule.matches_domain?("v2.api.example.com")
|
|
end
|
|
|
|
test "matches_domain? should handle case insensitivity" do
|
|
@rule.update!(domain_pattern: "*.EXAMPLE.COM")
|
|
@rule.save!
|
|
|
|
assert @rule.matches_domain?("app.example.com")
|
|
assert @rule.matches_domain?("APP.EXAMPLE.COM")
|
|
assert @rule.matches_domain?("App.Example.Com")
|
|
end
|
|
|
|
test "matches_domain? should handle empty and nil domains" do
|
|
@rule.save!
|
|
|
|
assert_not @rule.matches_domain?("")
|
|
assert_not @rule.matches_domain?(nil)
|
|
end
|
|
|
|
# Advanced Header Configuration Tests
|
|
test "headers_for_user should handle partial header configuration" do
|
|
user = users(:one)
|
|
user.groups << groups(:one)
|
|
@rule.update!(headers_config: {
|
|
user: "X-Custom-User",
|
|
email: "", # Disabled
|
|
groups: "X-Custom-Groups"
|
|
})
|
|
@rule.save!
|
|
|
|
headers = @rule.headers_for_user(user)
|
|
|
|
# Should include custom user header
|
|
assert_equal "X-Custom-User", headers.keys.find { |k| k.include?("User") }
|
|
assert_equal user.email_address, headers["X-Custom-User"]
|
|
|
|
# Should include default email header (not overridden)
|
|
assert_equal "X-Remote-Email", headers.keys.find { |k| k.include?("Email") }
|
|
assert_equal user.email_address, headers["X-Remote-Email"]
|
|
|
|
# Should include custom groups header
|
|
assert_equal "X-Custom-Groups", headers.keys.find { |k| k.include?("Groups") }
|
|
assert_equal groups(:one).name, headers["X-Custom-Groups"]
|
|
|
|
# Should include default name header (not overridden)
|
|
assert_equal "X-Remote-Name", headers.keys.find { |k| k.include?("Name") }
|
|
end
|
|
|
|
test "headers_for_user should handle user without groups when groups header configured" do
|
|
user = users(:one)
|
|
user.groups.clear # No groups
|
|
@rule.update!(headers_config: { groups: "X-Custom-Groups" })
|
|
@rule.save!
|
|
|
|
headers = @rule.headers_for_user(user)
|
|
|
|
# Should not include groups header for user with no groups
|
|
assert_nil headers["X-Custom-Groups"]
|
|
assert_nil headers["X-Remote-Groups"]
|
|
end
|
|
|
|
test "headers_for_user should handle non-admin user correctly" do
|
|
user = users(:one)
|
|
# Ensure user is not admin
|
|
user.update!(admin: false)
|
|
@rule.save!
|
|
|
|
headers = @rule.headers_for_user(user)
|
|
|
|
assert_equal "false", headers["X-Remote-Admin"]
|
|
end
|
|
|
|
test "headers_for_user should work with nil headers_config" do
|
|
user = users(:one)
|
|
@rule.update!(headers_config: nil)
|
|
@rule.save!
|
|
|
|
headers = @rule.headers_for_user(user)
|
|
|
|
# Should use default headers
|
|
assert_equal "X-Remote-User", headers.keys.find { |k| k.include?("User") }
|
|
assert_equal user.email_address, headers["X-Remote-User"]
|
|
end
|
|
|
|
test "effective_headers should handle symbol keys in headers_config" do
|
|
@rule.update!(headers_config: { user: "X-Symbol-User", email: "X-Symbol-Email" })
|
|
@rule.save!
|
|
|
|
effective = @rule.effective_headers
|
|
|
|
assert_equal "X-Symbol-User", effective[:user]
|
|
assert_equal "X-Symbol-Email", effective[:email]
|
|
assert_equal "X-Remote-Name", effective[:name] # Default
|
|
end
|
|
|
|
test "effective_headers should handle string keys in headers_config" do
|
|
@rule.update!(headers_config: { "user" => "X-String-User", "email" => "X-String-Email" })
|
|
@rule.save!
|
|
|
|
effective = @rule.effective_headers
|
|
|
|
assert_equal "X-String-User", effective[:user]
|
|
assert_equal "X-String-Email", effective[:email]
|
|
assert_equal "X-Remote-Name", effective[:name] # Default
|
|
end
|
|
|
|
# Policy and Access Control Tests
|
|
test "policy_for_user should handle user with TOTP enabled" do
|
|
user = users(:one)
|
|
user.update!(totp_secret: "test_secret")
|
|
@rule.allowed_groups << groups(:one)
|
|
user.groups << groups(:one)
|
|
@rule.save!
|
|
|
|
policy = @rule.policy_for_user(user)
|
|
assert_equal "two_factor", policy
|
|
end
|
|
|
|
test "policy_for_user should handle user without TOTP" do
|
|
user = users(:one)
|
|
user.update!(totp_secret: nil)
|
|
@rule.allowed_groups << groups(:one)
|
|
user.groups << groups(:one)
|
|
@rule.save!
|
|
|
|
policy = @rule.policy_for_user(user)
|
|
assert_equal "one_factor", policy
|
|
end
|
|
|
|
test "policy_for_user should handle user with multiple groups" do
|
|
user = users(:one)
|
|
group1 = groups(:one)
|
|
group2 = groups(:two)
|
|
@rule.allowed_groups << group1
|
|
@rule.allowed_groups << group2
|
|
user.groups << group1
|
|
@rule.save!
|
|
|
|
policy = @rule.policy_for_user(user)
|
|
assert_equal "one_factor", policy
|
|
end
|
|
|
|
test "user_allowed? should handle user with multiple groups, one allowed" do
|
|
user = users(:one)
|
|
allowed_group = groups(:one)
|
|
other_group = groups(:two)
|
|
@rule.allowed_groups << allowed_group
|
|
user.groups << allowed_group
|
|
user.groups << other_group
|
|
@rule.save!
|
|
|
|
assert @rule.user_allowed?(user)
|
|
end
|
|
|
|
test "user_allowed? should handle user with multiple groups, none allowed" do
|
|
user = users(:one)
|
|
group1 = groups(:one)
|
|
group2 = groups(:two)
|
|
# Don't add any groups to allowed_groups
|
|
user.groups << group1
|
|
user.groups << group2
|
|
@rule.save!
|
|
|
|
assert_not @rule.user_allowed?(user)
|
|
end
|
|
end
|