use feature_flag::{FlagEvaluator, FlagSet, Subject};
use serde_json::json;
fn build(set: &str) -> FlagEvaluator {
FlagEvaluator::new(FlagSet::from_json(set).expect("valid"))
}
const ALWAYS_ON: &str = r#"{
"flags": [{
"id": "f",
"variants": ["on", "off"],
"default_variant": "off",
"rules": [
{"id": "r", "when": {"kind": "always"},
"outcome": {"kind": "variant", "variant": "on"}}
]
}]
}"#;
#[test]
fn rule_match_returns_rule_variant() {
let e = build(ALWAYS_ON);
let r = e.evaluate("f", &Subject::new("u1")).unwrap();
assert_eq!(r.variant, "on");
assert_eq!(r.matched_rule_id.as_deref(), Some("r"));
assert_eq!(r.reason, "rule_match");
}
#[test]
fn default_returned_when_no_rule_matches() {
let set = r#"{
"flags": [{
"id": "f",
"variants": ["on", "off"],
"default_variant": "off",
"rules": [
{"id": "us-only",
"when": {"kind": "compare", "attr": "country", "op": "eq", "value": "US"},
"outcome": {"kind": "variant", "variant": "on"}}
]
}]
}"#;
let e = build(set);
let r = e
.evaluate("f", &Subject::new("u1").with_attr("country", "DE"))
.unwrap();
assert_eq!(r.variant, "off");
assert_eq!(r.reason, "default");
assert!(r.matched_rule_id.is_none());
}
#[test]
fn disabled_flag_skips_rules() {
let set = r#"{
"flags": [{
"id": "f",
"enabled": false,
"variants": ["on", "off"],
"default_variant": "off",
"rules": [
{"id": "r", "when": {"kind": "always"},
"outcome": {"kind": "variant", "variant": "on"}}
]
}]
}"#;
let e = build(set);
let r = e.evaluate("f", &Subject::new("u1")).unwrap();
assert_eq!(r.variant, "off");
assert_eq!(r.reason, "disabled");
}
#[test]
fn unknown_flag_yields_error() {
let e = build(ALWAYS_ON);
let err = e.evaluate("missing", &Subject::new("u")).unwrap_err();
assert!(matches!(
err,
feature_flag::FeatureFlagError::UnknownFlag(_)
));
}
#[test]
fn variant_or_falls_back_for_unknown_flag() {
let e = build(ALWAYS_ON);
let v = e.variant_or("missing", &Subject::new("u"), "off");
assert_eq!(v, "off");
}
#[test]
fn rollout_distributes_buckets_sticky() {
let set = r#"{
"flags": [{
"id": "f",
"variants": ["on", "off"],
"default_variant": "off",
"rules": [{
"id": "rollout",
"when": {"kind": "always"},
"outcome": {
"kind": "rollout",
"variants": [
{"variant": "on", "weight": 50},
{"variant": "off", "weight": 50}
]
}
}]
}]
}"#;
let e = build(set);
let s = Subject::new("user-42");
let first = e.evaluate("f", &s).unwrap().variant;
for _ in 0..50 {
assert_eq!(e.evaluate("f", &s).unwrap().variant, first);
}
}
#[test]
fn rollout_distributes_roughly_evenly_over_many_subjects() {
let set = r#"{
"flags": [{
"id": "f",
"variants": ["a", "b", "c"],
"default_variant": "a",
"rules": [{
"id": "rollout",
"when": {"kind": "always"},
"outcome": {
"kind": "rollout",
"variants": [
{"variant": "a", "weight": 33},
{"variant": "b", "weight": 33},
{"variant": "c", "weight": 34}
]
}
}]
}]
}"#;
let e = build(set);
let mut a = 0;
let mut b = 0;
let mut c = 0;
for i in 0..10_000 {
let v = e
.evaluate("f", &Subject::new(format!("u-{i}")))
.unwrap()
.variant;
match v.as_str() {
"a" => a += 1,
"b" => b += 1,
"c" => c += 1,
other => panic!("unexpected variant {other}"),
}
}
for (label, n) in [("a", a), ("b", b), ("c", c)] {
assert!(
(2900..=3700).contains(&n),
"variant {label} got {n} subjects — distribution looks off"
);
}
}
#[test]
fn predicate_eq_attribute() {
let set = r#"{
"flags": [{
"id": "f",
"variants": ["on", "off"],
"default_variant": "off",
"rules": [{
"id": "plan-pro",
"when": {"kind": "compare", "attr": "plan", "op": "eq", "value": "pro"},
"outcome": {"kind": "variant", "variant": "on"}
}]
}]
}"#;
let e = build(set);
assert_eq!(
e.evaluate("f", &Subject::new("u").with_attr("plan", "pro"))
.unwrap()
.variant,
"on"
);
assert_eq!(
e.evaluate("f", &Subject::new("u").with_attr("plan", "free"))
.unwrap()
.variant,
"off"
);
}
#[test]
fn predicate_all_of_and_in() {
let set = r#"{
"flags": [{
"id": "f",
"variants": ["on", "off"],
"default_variant": "off",
"rules": [{
"id": "pro-in-us-or-ca",
"when": {
"kind": "all_of",
"matchers": [
{"kind": "compare", "attr": "plan", "op": "eq", "value": "pro"},
{"kind": "compare", "attr": "country", "op": "in",
"value": ["US", "CA"]}
]
},
"outcome": {"kind": "variant", "variant": "on"}
}]
}]
}"#;
let e = build(set);
let sub = |c: &str| {
Subject::new("u")
.with_attr("plan", "pro")
.with_attr("country", c)
};
assert_eq!(e.evaluate("f", &sub("US")).unwrap().variant, "on");
assert_eq!(e.evaluate("f", &sub("CA")).unwrap().variant, "on");
assert_eq!(e.evaluate("f", &sub("DE")).unwrap().variant, "off");
}
#[test]
fn predicate_numeric_comparison() {
let set = r#"{
"flags": [{
"id": "f",
"variants": ["on", "off"],
"default_variant": "off",
"rules": [{
"id": "vip",
"when": {"kind": "compare", "attr": "ltv", "op": "gte", "value": 1000},
"outcome": {"kind": "variant", "variant": "on"}
}]
}]
}"#;
let e = build(set);
let s = Subject::new("u").with_attr("ltv", json!(1500));
assert_eq!(e.evaluate("f", &s).unwrap().variant, "on");
let s = Subject::new("u").with_attr("ltv", json!(500));
assert_eq!(e.evaluate("f", &s).unwrap().variant, "off");
}
#[test]
fn swap_replaces_loaded_flagset() {
let e = build(ALWAYS_ON);
assert_eq!(e.evaluate("f", &Subject::new("u")).unwrap().variant, "on");
let off_set = r#"{
"flags": [{
"id": "f",
"variants": ["on", "off"],
"default_variant": "off",
"rules": []
}]
}"#;
e.swap(FlagSet::from_json(off_set).unwrap());
assert_eq!(e.evaluate("f", &Subject::new("u")).unwrap().variant, "off");
}