use std::sync::Arc;
use laye::{
policy::{AccessPolicy, AccessRule},
principal::Principal,
result::LayeCheckResult,
};
struct TestUser {
roles: Vec<String>,
permissions: Vec<String>,
authenticated: bool,
}
impl Principal for TestUser {
fn roles(&self) -> &[String] {
&self.roles
}
fn permissions(&self) -> &[String] {
&self.permissions
}
fn is_authenticated(&self) -> bool {
self.authenticated
}
}
fn make_user(roles: &[&str], permissions: &[&str]) -> TestUser {
TestUser {
roles: roles.iter().map(|s| s.to_string()).collect(),
permissions: permissions.iter().map(|s| s.to_string()).collect(),
authenticated: true,
}
}
fn make_guest() -> TestUser {
TestUser {
roles: vec![],
permissions: vec![],
authenticated: false,
}
}
#[test]
fn role_rule_passes_for_matching_role() {
let user = make_user(&["admin"], &[]);
let policy = AccessPolicy::require_all().add_rule(AccessRule::Role("admin".into()));
assert_eq!(
policy.check(Some(&user)),
LayeCheckResult::Authorized,
"admin user should pass Role('admin') policy"
);
}
#[test]
fn role_rule_fails_for_wrong_role() {
let user = make_user(&["editor"], &[]);
let policy = AccessPolicy::require_all().add_rule(AccessRule::Role("admin".into()));
assert_eq!(
policy.check(Some(&user)),
LayeCheckResult::Forbidden,
"editor user should get Forbidden for Role('admin') policy"
);
}
#[test]
fn role_rule_fails_for_unauthenticated_returns_unauthorized() {
let policy = AccessPolicy::require_all().add_rule(AccessRule::Role("admin".into()));
assert_eq!(
policy.check(None),
LayeCheckResult::Unauthorized,
"missing principal should get Unauthorized for Role policy"
);
}
#[test]
fn permission_rule_passes_for_matching_permission() {
let user = make_user(&[], &["write"]);
let policy = AccessPolicy::require_all().add_rule(AccessRule::Permission("write".into()));
assert_eq!(
policy.check(Some(&user)),
LayeCheckResult::Authorized,
"user with 'write' should pass Permission('write') policy"
);
}
#[test]
fn permission_rule_fails_for_wrong_permission() {
let user = make_user(&[], &["read"]);
let policy = AccessPolicy::require_all().add_rule(AccessRule::Permission("write".into()));
assert_eq!(
policy.check(Some(&user)),
LayeCheckResult::Forbidden,
"user with only 'read' should get Forbidden for Permission('write') policy"
);
}
#[test]
fn authenticated_rule_passes_for_authenticated_principal() {
let user = make_user(&[], &[]);
let policy = AccessPolicy::require_all().add_rule(AccessRule::Authenticated);
assert_eq!(
policy.check(Some(&user)),
LayeCheckResult::Authorized,
"authenticated user should pass Authenticated rule"
);
}
#[test]
fn authenticated_rule_fails_returning_unauthorized_for_none() {
let policy = AccessPolicy::require_all().add_rule(AccessRule::Authenticated);
assert_eq!(
policy.check(None),
LayeCheckResult::Unauthorized,
"missing principal should get Unauthorized for Authenticated rule"
);
}
#[test]
fn authenticated_rule_fails_returning_unauthenticated_for_unauthenticated_principal() {
let guest = make_guest();
let policy = AccessPolicy::require_all().add_rule(AccessRule::Authenticated);
assert_eq!(
policy.check(Some(&guest)),
LayeCheckResult::Unauthorized,
"unauthenticated principal should get Unauthorized for Authenticated rule"
);
}
#[test]
fn guest_rule_passes_for_none() {
let policy = AccessPolicy::require_all().add_rule(AccessRule::Guest);
assert_eq!(
policy.check(None),
LayeCheckResult::Authorized,
"None principal should pass Guest rule"
);
}
#[test]
fn guest_rule_passes_for_unauthenticated_principal() {
let guest = make_guest();
let policy = AccessPolicy::require_all().add_rule(AccessRule::Guest);
assert_eq!(
policy.check(Some(&guest)),
LayeCheckResult::Authorized,
"unauthenticated principal should pass Guest rule"
);
}
#[test]
fn guest_rule_fails_for_authenticated_principal() {
let user = make_user(&[], &[]);
let policy = AccessPolicy::require_all().add_rule(AccessRule::Guest);
assert_eq!(
policy.check(Some(&user)),
LayeCheckResult::Forbidden,
"authenticated principal should get Forbidden for Guest rule"
);
}
#[test]
fn custom_rule_delegates_to_closure() {
let user = make_user(&[], &[]);
let policy = AccessPolicy::require_all().add_rule(AccessRule::Custom(Arc::new(|_| true)));
assert_eq!(
policy.check(Some(&user)),
LayeCheckResult::Authorized,
"custom rule returning true should pass"
);
let policy = AccessPolicy::require_all().add_rule(AccessRule::Custom(Arc::new(|_| false)));
assert_eq!(
policy.check(Some(&user)),
LayeCheckResult::Forbidden,
"custom rule returning false with present principal should give Forbidden"
);
}
#[test]
fn custom_rule_receives_none_for_missing_principal() {
let called_with_none = Arc::new(std::sync::Mutex::new(false));
let flag = called_with_none.clone();
let policy = AccessPolicy::require_all().add_rule(AccessRule::Custom(Arc::new(move |p| {
if p.is_none() {
*flag.lock().unwrap() = true;
}
false
})));
let _ = policy.check(None);
assert!(
*called_with_none.lock().unwrap(),
"custom closure should receive None when no principal"
);
}
#[test]
fn not_role_passes_when_principal_lacks_role() {
let user = make_user(&["editor"], &[]);
let policy = AccessPolicy::require_all().add_rule(AccessRule::NotRole("banned".into()));
assert_eq!(
policy.check(Some(&user)),
LayeCheckResult::Authorized,
"user without 'banned' role should pass NotRole('banned') policy"
);
}
#[test]
fn not_role_fails_when_principal_has_role() {
let user = make_user(&["banned"], &[]);
let policy = AccessPolicy::require_all().add_rule(AccessRule::NotRole("banned".into()));
assert_eq!(
policy.check(Some(&user)),
LayeCheckResult::Forbidden,
"user with 'banned' role should get Forbidden for NotRole('banned') policy"
);
}
#[test]
fn not_role_fails_for_missing_principal() {
let policy = AccessPolicy::require_all().add_rule(AccessRule::NotRole("banned".into()));
assert_eq!(
policy.check(None),
LayeCheckResult::Unauthorized,
"missing principal should get Unauthorized for NotRole policy"
);
}
#[test]
fn not_permission_passes_when_principal_lacks_permission() {
let user = make_user(&[], &["read"]);
let policy = AccessPolicy::require_all().add_rule(AccessRule::NotPermission("delete".into()));
assert_eq!(
policy.check(Some(&user)),
LayeCheckResult::Authorized,
"user without 'delete' permission should pass NotPermission('delete') policy"
);
}
#[test]
fn not_permission_fails_when_principal_has_permission() {
let user = make_user(&[], &["delete"]);
let policy = AccessPolicy::require_all().add_rule(AccessRule::NotPermission("delete".into()));
assert_eq!(
policy.check(Some(&user)),
LayeCheckResult::Forbidden,
"user with 'delete' permission should get Forbidden for NotPermission('delete') policy"
);
}
#[test]
fn absence_check_composes_with_presence_check() {
let user = make_user(&["editor"], &[]);
let policy = AccessPolicy::require_all()
.add_rule(AccessRule::Authenticated)
.add_rule(AccessRule::NotRole("banned".into()));
assert_eq!(
policy.check(Some(&user)),
LayeCheckResult::Authorized,
"authenticated user without banned role should pass combined presence+absence policy"
);
let banned = make_user(&["editor", "banned"], &[]);
assert_eq!(
policy.check(Some(&banned)),
LayeCheckResult::Forbidden,
"authenticated user with banned role should fail combined policy"
);
}
#[test]
fn require_all_passes_when_all_rules_pass() {
let user = make_user(&["admin"], &["write"]);
let policy = AccessPolicy::require_all()
.add_rule(AccessRule::Authenticated)
.add_rule(AccessRule::Role("admin".into()))
.add_rule(AccessRule::Permission("write".into()));
assert_eq!(
policy.check(Some(&user)),
LayeCheckResult::Authorized,
"user satisfying all rules should pass require_all"
);
}
#[test]
fn require_all_fails_when_any_rule_fails() {
let user = make_user(&["admin"], &[]);
let policy = AccessPolicy::require_all()
.add_rule(AccessRule::Role("admin".into()))
.add_rule(AccessRule::Permission("write".into()));
assert_eq!(
policy.check(Some(&user)),
LayeCheckResult::Forbidden,
"require_all should fail if any single rule fails"
);
}
#[test]
fn require_any_passes_when_one_rule_passes() {
let user = make_user(&["editor"], &[]);
let policy = AccessPolicy::require_any()
.add_rule(AccessRule::Role("admin".into()))
.add_rule(AccessRule::Role("editor".into()));
assert_eq!(
policy.check(Some(&user)),
LayeCheckResult::Authorized,
"require_any should pass when at least one rule passes"
);
}
#[test]
fn require_any_fails_when_all_rules_fail() {
let user = make_user(&["viewer"], &[]);
let policy = AccessPolicy::require_any()
.add_rule(AccessRule::Role("admin".into()))
.add_rule(AccessRule::Role("editor".into()));
assert_eq!(
policy.check(Some(&user)),
LayeCheckResult::Forbidden,
"require_any should fail when all rules fail"
);
}
#[test]
fn nested_policy_and_of_or_passes() {
let user = make_user(&["editor"], &["publish"]);
let inner = AccessPolicy::require_any()
.add_rule(AccessRule::Role("admin".into()))
.add_rule(AccessRule::Role("editor".into()));
let policy = AccessPolicy::require_all()
.add_policy(inner)
.add_rule(AccessRule::Permission("publish".into()));
assert_eq!(
policy.check(Some(&user)),
LayeCheckResult::Authorized,
"editor with publish perm should pass AND(OR(admin|editor), publish)"
);
}
#[test]
fn nested_policy_or_of_and_fails_correctly() {
let user = make_user(&["admin"], &[]);
let first = AccessPolicy::require_all()
.add_rule(AccessRule::Role("admin".into()))
.add_rule(AccessRule::Permission("delete".into()));
let second = AccessPolicy::require_all()
.add_rule(AccessRule::Role("superuser".into()))
.add_rule(AccessRule::Permission("nuke".into()));
let policy = AccessPolicy::require_any()
.add_policy(first)
.add_policy(second);
assert_eq!(
policy.check(Some(&user)),
LayeCheckResult::Forbidden,
"OR of two failing AND policies should return Forbidden for authenticated user"
);
}
#[test]
fn unauthenticated_request_yields_unauthorized_not_forbidden() {
let policy = AccessPolicy::require_all().add_rule(AccessRule::Role("admin".into()));
assert_eq!(
policy.check(None),
LayeCheckResult::Unauthorized,
"no principal should yield Unauthorized, not Forbidden"
);
}
#[test]
fn authenticated_without_role_yields_forbidden_not_unauthorized() {
let user = make_user(&[], &[]);
let policy = AccessPolicy::require_all().add_rule(AccessRule::Role("admin".into()));
assert_eq!(
policy.check(Some(&user)),
LayeCheckResult::Forbidden,
"authenticated user lacking required role should yield Forbidden, not Unauthorized"
);
}