use ooroo::{
bound_field, field, rule_ref, Bound, Context, RuleSet, RuleSetBuilder, Value, Verdict,
};
#[test]
fn is_in_field_matches_when_value_in_list() {
let ruleset = RuleSetBuilder::new()
.rule("r", |r| r.when(field("role").is_in_field("allowed_roles")))
.terminal("r", 0)
.compile()
.unwrap();
let ctx = Context::new().set("role", "editor").set(
"allowed_roles",
Value::List(vec![
Value::String("admin".into()),
Value::String("editor".into()),
]),
);
assert_eq!(ruleset.evaluate(&ctx), Some(Verdict::new("r", true)));
}
#[test]
fn is_in_field_no_match_returns_none() {
let ruleset = RuleSetBuilder::new()
.rule("r", |r| r.when(field("role").is_in_field("allowed_roles")))
.terminal("r", 0)
.compile()
.unwrap();
let ctx = Context::new().set("role", "guest").set(
"allowed_roles",
Value::List(vec![
Value::String("admin".into()),
Value::String("editor".into()),
]),
);
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn is_in_field_missing_list_field_returns_none() {
let ruleset = RuleSetBuilder::new()
.rule("r", |r| r.when(field("role").is_in_field("allowed_roles")))
.terminal("r", 0)
.compile()
.unwrap();
let ctx = Context::new().set("role", "admin");
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn is_in_field_empty_list_returns_none() {
let ruleset = RuleSetBuilder::new()
.rule("r", |r| r.when(field("role").is_in_field("allowed_roles")))
.terminal("r", 0)
.compile()
.unwrap();
let ctx = Context::new()
.set("role", "admin")
.set("allowed_roles", Value::List(vec![]));
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn in_bound_field_expands_list_match() {
let ruleset = RuleSetBuilder::new()
.rule("r", |r| {
r.when(field("tag").is_in([bound_field("allowed_tags")]))
})
.terminal("r", 0)
.compile()
.unwrap();
let ctx = Context::new().set("tag", "rust").set(
"allowed_tags",
Value::List(vec![
Value::String("rust".into()),
Value::String("systems".into()),
]),
);
assert_eq!(ruleset.evaluate(&ctx), Some(Verdict::new("r", true)));
}
#[test]
fn not_in_bound_field_expands_list() {
let ruleset = RuleSetBuilder::new()
.rule("r", |r| {
r.when(field("status").not_in([bound_field("blocked_statuses")]))
})
.terminal("r", 0)
.compile()
.unwrap();
let ctx_ok = Context::new().set("status", "active").set(
"blocked_statuses",
Value::List(vec![
Value::String("banned".into()),
Value::String("suspended".into()),
]),
);
assert_eq!(ruleset.evaluate(&ctx_ok), Some(Verdict::new("r", true)));
let ctx_blocked = Context::new().set("status", "banned").set(
"blocked_statuses",
Value::List(vec![
Value::String("banned".into()),
Value::String("suspended".into()),
]),
);
assert!(ruleset.evaluate(&ctx_blocked).is_none());
}
#[test]
fn in_literal_and_list_field_bound() {
let ruleset = RuleSetBuilder::new()
.rule("r", |r| {
r.when(field("role").is_in([Bound::from("superuser"), bound_field("allowed_roles")]))
})
.terminal("r", 0)
.compile()
.unwrap();
let ctx = Context::new().set("role", "superuser").set(
"allowed_roles",
Value::List(vec![Value::String("editor".into())]),
);
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new().set("role", "editor").set(
"allowed_roles",
Value::List(vec![Value::String("editor".into())]),
);
assert!(ruleset.evaluate(&ctx).is_some());
let ctx = Context::new().set("role", "guest").set(
"allowed_roles",
Value::List(vec![Value::String("editor".into())]),
);
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn dsl_list_literal_eq_match() {
let ruleset = RuleSet::from_dsl("rule r (priority 0):\n codes == [1, 2, 3]").unwrap();
let ctx = Context::new().set(
"codes",
Value::List(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
);
assert!(ruleset.evaluate(&ctx).is_some());
let ctx_wrong = Context::new().set("codes", Value::List(vec![Value::Int(1), Value::Int(2)]));
assert!(ruleset.evaluate(&ctx_wrong).is_none());
}
#[test]
fn dsl_list_literal_mixed_types() {
let ruleset = RuleSet::from_dsl(
r#"rule r (priority 0):
x == [1, "hello", true]"#,
)
.unwrap();
let ctx = Context::new().set(
"x",
Value::List(vec![
Value::Int(1),
Value::String("hello".into()),
Value::Bool(true),
]),
);
assert!(ruleset.evaluate(&ctx).is_some());
}
#[test]
fn dsl_empty_list_literal() {
let ruleset = RuleSet::from_dsl("rule r (priority 0):\n tags == []").unwrap();
let ctx_match = Context::new().set("tags", Value::List(vec![]));
assert!(ruleset.evaluate(&ctx_match).is_some());
let ctx_no_match = Context::new().set("tags", Value::List(vec![Value::Int(1)]));
assert!(ruleset.evaluate(&ctx_no_match).is_none());
}
#[test]
fn is_in_field_composes_with_and() {
let ruleset = RuleSetBuilder::new()
.rule("role_ok", |r| {
r.when(field("role").is_in_field("allowed_roles"))
})
.rule("region_ok", |r| {
r.when(field("region").is_in(["us-east", "us-west"]))
})
.rule("allowed", |r| {
r.when(rule_ref("role_ok").and(rule_ref("region_ok")))
})
.terminal("allowed", 0)
.compile()
.unwrap();
let ctx = Context::new()
.set("role", "admin")
.set(
"allowed_roles",
Value::List(vec![Value::String("admin".into())]),
)
.set("region", "us-east");
assert_eq!(ruleset.evaluate(&ctx), Some(Verdict::new("allowed", true)));
let ctx = Context::new()
.set("role", "admin")
.set(
"allowed_roles",
Value::List(vec![Value::String("admin".into())]),
)
.set("region", "eu-west");
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn is_in_field_composes_with_not() {
let ruleset = RuleSetBuilder::new()
.rule("blocked", |r| {
r.when(field("role").is_in_field("blocked_roles"))
})
.rule("allowed", |r| r.when(!rule_ref("blocked")))
.terminal("allowed", 0)
.compile()
.unwrap();
let ctx_ok = Context::new().set("role", "viewer").set(
"blocked_roles",
Value::List(vec![Value::String("banned".into())]),
);
assert!(ruleset.evaluate(&ctx_ok).is_some());
let ctx_blocked = Context::new().set("role", "banned").set(
"blocked_roles",
Value::List(vec![Value::String("banned".into())]),
);
assert!(ruleset.evaluate(&ctx_blocked).is_none());
}
#[test]
fn list_field_absent_is_false_not_panic() {
let ruleset = RuleSetBuilder::new()
.rule("r", |r| r.when(field("role").is_in_field("allowed_roles")))
.terminal("r", 0)
.compile()
.unwrap();
assert!(ruleset.evaluate(&Context::new()).is_none());
assert!(ruleset
.evaluate(&Context::new().set("role", "admin"))
.is_none());
assert!(ruleset
.evaluate(&Context::new().set(
"allowed_roles",
Value::List(vec![Value::String("admin".into())])
))
.is_none());
}
#[test]
fn list_value_ordering_ops_return_false() {
let ruleset = RuleSetBuilder::new()
.rule("r", |r| {
r.when(field("tags").gt(Value::List(vec![Value::Int(1)])))
})
.terminal("r", 0)
.compile()
.unwrap();
let ctx = Context::new().set("tags", Value::List(vec![Value::Int(1)]));
assert!(ruleset.evaluate(&ctx).is_none());
}
#[test]
fn rbac_scenario_list_based_permissions() {
let ruleset = RuleSetBuilder::new()
.rule("has_required_role", |r| {
r.when(field("user.primary_role").is_in_field("resource.required_roles"))
})
.rule("not_suspended", |r| {
r.when(!field("user.suspended").eq(true))
})
.rule("grant", |r| {
r.when(rule_ref("has_required_role").and(rule_ref("not_suspended")))
})
.rule("deny_suspended", |r| {
r.when(field("user.suspended").eq(true))
})
.terminal("deny_suspended", 0)
.terminal("grant", 10)
.compile()
.unwrap();
let required = Value::List(vec![
Value::String("admin".into()),
Value::String("editor".into()),
]);
let ctx = Context::new()
.set("user.primary_role", "admin")
.set("user.suspended", false)
.set("resource.required_roles", required.clone());
assert_eq!(ruleset.evaluate(&ctx), Some(Verdict::new("grant", true)));
let ctx = Context::new()
.set("user.primary_role", "viewer")
.set("user.suspended", false)
.set("resource.required_roles", required.clone());
assert!(ruleset.evaluate(&ctx).is_none());
let ctx = Context::new()
.set("user.primary_role", "admin")
.set("user.suspended", true)
.set("resource.required_roles", required.clone());
assert_eq!(
ruleset.evaluate(&ctx),
Some(Verdict::new("deny_suspended", true))
);
}