feature_probe_server_sdk/
evaluate.rs

1use crate::user::FPUser;
2use crate::FPError;
3use crate::{unix_timestamp, PrerequisiteError};
4use byteorder::{BigEndian, ReadBytesExt};
5use regex::Regex;
6use semver::Version;
7use serde::{Deserialize, Serialize};
8use serde_json::Value;
9use sha1::Digest;
10use std::string::String;
11use std::{collections::HashMap, str::FromStr};
12use tracing::{info, warn};
13
14#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
15#[serde(rename_all = "camelCase")]
16pub enum Serve {
17    Select(usize),
18    Split(Distribution),
19}
20
21#[derive(Serialize, Deserialize, Debug, Clone)]
22pub struct Variation {
23    pub value: Value,
24    pub index: usize,
25}
26
27impl Serve {
28    pub fn select_variation(&self, eval_param: &EvalParams) -> Result<Variation, FPError> {
29        let variations = eval_param.variations;
30        let index = match self {
31            Serve::Select(i) => *i,
32            Serve::Split(distribution) => distribution.find_index(eval_param)?,
33        };
34
35        match variations.get(index) {
36            None if eval_param.is_detail => Err(FPError::EvalDetailError(format!(
37                "index {} overflow, variations count is {}",
38                index,
39                variations.len()
40            ))),
41            None => Err(FPError::EvalError),
42            Some(v) => Ok(Variation {
43                value: v.clone(),
44                index,
45            }),
46        }
47    }
48}
49
50#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
51struct BucketRange((u32, u32));
52
53#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
54#[serde(rename_all = "camelCase")]
55pub struct Distribution {
56    distribution: Vec<Vec<BucketRange>>,
57    bucket_by: Option<String>,
58    salt: Option<String>,
59}
60
61impl Distribution {
62    pub fn find_index(&self, eval_param: &EvalParams) -> Result<usize, FPError> {
63        let user = eval_param.user;
64
65        let hash_key = match &self.bucket_by {
66            None => user.key(),
67            Some(custom_key) => match user.get(custom_key) {
68                None if eval_param.is_detail => {
69                    return Err(FPError::EvalDetailError(format!(
70                        "User with key:{:?} does not have attribute named: [{}]",
71                        user.key(),
72                        custom_key
73                    )));
74                }
75                None => return Err(FPError::EvalError),
76                Some(value) => value.to_owned(),
77            },
78        };
79
80        let salt = match &self.salt {
81            Some(s) if !s.is_empty() => s,
82            _ => eval_param.key,
83        };
84
85        let bucket_index = salt_hash(&hash_key, salt, 10000);
86
87        let variation = self.distribution.iter().position(|ranges| {
88            ranges.iter().any(|pair| {
89                let (lower, upper) = pair.0;
90                lower <= bucket_index && bucket_index < upper
91            })
92        });
93
94        match variation {
95            None if eval_param.is_detail => Err(FPError::EvalDetailError(
96                "not find hash_bucket in distribution.".to_string(),
97            )),
98            None => Err(FPError::EvalError),
99            Some(index) => Ok(index),
100        }
101    }
102}
103
104fn salt_hash(key: &str, salt: &str, bucket_size: u64) -> u32 {
105    let size = 4;
106    let mut hasher = sha1::Sha1::new();
107    let data = format!("{key}{salt}");
108    hasher.update(data);
109    let hax_value = hasher.finalize();
110    let mut v = Vec::with_capacity(size);
111    for i in (hax_value.len() - size)..hax_value.len() {
112        v.push(hax_value[i]);
113    }
114    let mut v = v.as_slice();
115    let value = v.read_u32::<BigEndian>().expect("can not be here");
116    value % bucket_size as u32
117}
118
119pub struct EvalParams<'a> {
120    key: &'a str,
121    is_detail: bool,
122    user: &'a FPUser,
123    variations: &'a [Value],
124    segment_repo: &'a HashMap<String, Segment>,
125    toggle_repo: &'a HashMap<String, Toggle>,
126    debug_until_time: Option<u64>,
127}
128
129#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Default, Clone)]
130#[serde(rename_all = "camelCase")]
131pub struct EvalDetail<T> {
132    pub value: Option<T>,
133    pub rule_index: Option<usize>,
134    pub track_access_events: Option<bool>,
135    pub debug_until_time: Option<u64>,
136    pub last_modified: Option<u64>,
137    pub variation_index: Option<usize>,
138    pub version: Option<u64>,
139    pub reason: String,
140}
141
142#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
143#[serde(rename_all = "camelCase")]
144pub struct Prerequisites {
145    pub key: String,
146    pub value: Value,
147}
148
149#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
150#[serde(rename_all = "camelCase")]
151pub struct Toggle {
152    key: String,
153    enabled: bool,
154    track_access_events: Option<bool>,
155    last_modified: Option<u64>,
156    version: u64,
157    for_client: bool,
158    disabled_serve: Serve,
159    default_serve: Serve,
160    rules: Vec<Rule>,
161    variations: Vec<Value>,
162    prerequisites: Option<Vec<Prerequisites>>,
163}
164
165impl Toggle {
166    pub fn eval(
167        &self,
168        user: &FPUser,
169        segment_repo: &HashMap<String, Segment>,
170        toggle_repo: &HashMap<String, Toggle>,
171        is_detail: bool,
172        deep: u8,
173        debug_until_time: Option<u64>,
174    ) -> EvalDetail<Value> {
175        let eval_param = EvalParams {
176            user,
177            segment_repo,
178            toggle_repo,
179            key: &self.key,
180            is_detail,
181            variations: &self.variations,
182            debug_until_time,
183        };
184
185        match self.do_eval(&eval_param, deep) {
186            Ok(eval) => eval,
187            Err(e) => self.disabled_variation(&eval_param, Some(e.to_string())),
188        }
189    }
190
191    fn do_eval(
192        &self,
193        eval_param: &EvalParams,
194        max_depth: u8,
195    ) -> Result<EvalDetail<Value>, PrerequisiteError> {
196        if !self.enabled {
197            return Ok(self.disabled_variation(eval_param, None))
198        }
199
200        if !self.meet_prerequisite(eval_param, max_depth)? {
201            return Ok(self.disabled_variation(eval_param, Some(
202                "Prerequisite not match".to_owned())));
203        }
204
205        for (i, rule) in self.rules.iter().enumerate() {
206            match rule.serve_variation(eval_param) {
207                Ok(v) => {
208                    if v.is_some() {
209                        return Ok(self.serve_variation(
210                            v,
211                            format!("rule {i}"),
212                            Some(i),
213                            eval_param.debug_until_time,
214                        ));
215                    }
216                }
217                Err(e) => {
218                    return Ok(self.serve_variation(
219                        None,
220                        format!("{e:?}"),
221                        Some(i),
222                        eval_param.debug_until_time,
223                    ));
224                }
225            }
226        }
227
228        Ok(self.default_variation(eval_param, None))
229    }
230
231    fn meet_prerequisite(
232        &self,
233        eval_param: &EvalParams,
234        deep: u8,
235    ) -> Result<bool, PrerequisiteError> {
236        if deep == 0 {
237            return Err(PrerequisiteError::DepthOverflow);
238        }
239
240        if let Some(ref prerequisites) = self.prerequisites {
241            for pre in prerequisites {
242                let eval = match eval_param.toggle_repo.get(&pre.key) {
243                    None => {
244                        return Err(PrerequisiteError::NotExist(pre.key.to_string()));
245                    }
246                    Some(t) => t.do_eval(
247                        &EvalParams {
248                            key: &t.key,
249                            variations: &t.variations,
250                            is_detail: eval_param.is_detail,
251                            user: eval_param.user,
252                            segment_repo: eval_param.segment_repo,
253                            toggle_repo: eval_param.toggle_repo,
254                            debug_until_time: eval_param.debug_until_time,
255                        },
256                        deep - 1,
257                    )?,
258                };
259
260                match eval.value {
261                    Some(v) if v == pre.value => continue,
262                    _ => return Ok(false),
263                }
264            }
265            return Ok(true);
266        }
267        Ok(true)
268    }
269
270    fn serve_variation(
271        &self,
272        v: Option<Variation>,
273        reason: String,
274        rule_index: Option<usize>,
275        debug_until_time: Option<u64>,
276    ) -> EvalDetail<Value> {
277        EvalDetail {
278            variation_index: v.as_ref().map(|v| v.index),
279            value: v.map(|v| v.value),
280            version: Some(self.version),
281            track_access_events: self.track_access_events,
282            debug_until_time,
283            last_modified: self.last_modified,
284            rule_index,
285            reason,
286        }
287    }
288
289    fn default_variation(
290        &self,
291        eval_param: &EvalParams,
292        reason: Option<String>,
293    ) -> EvalDetail<Value> {
294        return self.fixed_variation(
295            &self.default_serve,
296            eval_param,
297            "default.".to_owned(),
298            reason,
299        );
300    }
301
302    fn disabled_variation(
303        &self,
304        eval_param: &EvalParams,
305        reason: Option<String>,
306    ) -> EvalDetail<Value> {
307        return self.fixed_variation(
308            &self.disabled_serve,
309            eval_param,
310            "disabled.".to_owned(),
311            reason,
312        );
313    }
314
315    fn fixed_variation(
316        &self,
317        serve: &Serve,
318        eval_param: &EvalParams,
319        default_reason: String,
320        reason: Option<String>,
321    ) -> EvalDetail<Value> {
322        match serve.select_variation(eval_param) {
323            Ok(v) => self.serve_variation(
324                Some(v),
325                concat_reason(default_reason, reason),
326                None,
327                eval_param.debug_until_time,
328            ),
329            Err(e) => self.serve_variation(
330                None,
331                concat_reason(format!("{e:?}"), reason),
332                None,
333                eval_param.debug_until_time,
334            ),
335        }
336    }
337
338    pub fn track_access_events(&self) -> bool {
339        self.track_access_events.unwrap_or(false)
340    }
341
342    #[cfg(feature = "internal")]
343    pub fn is_for_client(&self) -> bool {
344        self.for_client
345    }
346
347    #[cfg(feature = "internal")]
348    pub fn all_segment_ids(&self) -> Vec<&str> {
349        let mut sids: Vec<&str> = Vec::new();
350        for r in &self.rules {
351            for c in &r.conditions {
352                if c.r#type == ConditionType::Segment {
353                    sids.push(&c.subject)
354                }
355            }
356        }
357        sids
358    }
359
360    pub fn new_for_test(key: String, val: Value) -> Self {
361        Self {
362            key,
363            enabled: true,
364            track_access_events: None,
365            last_modified: None,
366            default_serve: Serve::Select(0),
367            disabled_serve: Serve::Select(0),
368            variations: vec![val],
369            version: 0,
370            for_client: false,
371            rules: vec![],
372            prerequisites: None,
373        }
374    }
375}
376
377#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
378struct SegmentRule {
379    conditions: Vec<Condition>,
380}
381
382impl SegmentRule {
383    pub fn allow(&self, user: &FPUser) -> bool {
384        for c in &self.conditions {
385            if c.meet(user, None) {
386                return true;
387            }
388        }
389        false
390    }
391}
392
393#[derive(Serialize, Deserialize, Debug)]
394struct DefaultRule {
395    pub serve: Serve,
396}
397
398#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
399struct Rule {
400    serve: Serve,
401    conditions: Vec<Condition>,
402}
403
404impl Rule {
405    pub fn serve_variation(&self, eval_param: &EvalParams) -> Result<Option<Variation>, FPError> {
406        let user = eval_param.user;
407        let segment_repo = eval_param.segment_repo;
408        match self
409            .conditions
410            .iter()
411            .all(|c| c.meet(user, Some(segment_repo)))
412        {
413            true => Ok(Some(self.serve.select_variation(eval_param)?)),
414            false => Ok(None),
415        }
416    }
417}
418
419#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
420#[serde(rename_all = "camelCase")]
421enum ConditionType {
422    String,
423    Segment,
424    Datetime,
425    Number,
426    Semver,
427    #[serde(other)]
428    Unknown,
429}
430
431#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
432struct Condition {
433    r#type: ConditionType,
434    #[serde(default)]
435    subject: String,
436    predicate: String,
437    objects: Vec<String>,
438}
439
440impl Condition {
441    pub fn meet(&self, user: &FPUser, segment_repo: Option<&HashMap<String, Segment>>) -> bool {
442        match &self.r#type {
443            ConditionType::String => self.match_string(user, &self.predicate),
444            ConditionType::Segment => self.match_segment(user, &self.predicate, segment_repo),
445            ConditionType::Number => self.match_ordering::<f64>(user, &self.predicate),
446            ConditionType::Semver => self.match_ordering::<Version>(user, &self.predicate),
447            ConditionType::Datetime => self.match_timestamp(user, &self.predicate),
448            _ => false,
449        }
450    }
451
452    fn match_segment(
453        &self,
454        user: &FPUser,
455        predicate: &str,
456        segment_repo: Option<&HashMap<String, Segment>>,
457    ) -> bool {
458        match segment_repo {
459            None => false,
460            Some(repo) => match predicate {
461                "is in" => self.user_in_segments(user, repo),
462                "is not in" => !self.user_in_segments(user, repo),
463                _ => false,
464            },
465        }
466    }
467
468    fn match_string(&self, user: &FPUser, predicate: &str) -> bool {
469        if let Some(c) = user.get(&self.subject) {
470            return match predicate {
471                "is one of" => self.do_match::<String>(c, |c, o| c.eq(o)),
472                "ends with" => self.do_match::<String>(c, |c, o| c.ends_with(o)),
473                "starts with" => self.do_match::<String>(c, |c, o| c.starts_with(o)),
474                "contains" => self.do_match::<String>(c, |c, o| c.contains(o)),
475                "matches regex" => {
476                    self.do_match::<String>(c, |c, o| match Regex::new(o) {
477                        Ok(re) => re.is_match(c),
478                        Err(_) => false, // invalid regex should be checked when load config
479                    })
480                }
481                "is not any of" => !self.match_string(user, "is one of"),
482                "does not end with" => !self.match_string(user, "ends with"),
483                "does not start with" => !self.match_string(user, "starts with"),
484                "does not contain" => !self.match_string(user, "contains"),
485                "does not match regex" => !self.match_string(user, "matches regex"),
486                _ => {
487                    info!("unknown predicate {}", predicate);
488                    false
489                }
490            };
491        }
492        info!("user attr missing: {}", self.subject);
493        false
494    }
495
496    fn match_ordering<T: FromStr + PartialOrd>(&self, user: &FPUser, predicate: &str) -> bool {
497        if let Some(c) = user.get(&self.subject) {
498            let c: T = match c.parse() {
499                Ok(v) => v,
500                Err(_) => return false,
501            };
502            return match predicate {
503                "=" => self.do_match::<T>(&c, |c, o| c.eq(o)),
504                "!=" => !self.match_ordering::<T>(user, "="),
505                ">" => self.do_match::<T>(&c, |c, o| c.gt(o)),
506                ">=" => self.do_match::<T>(&c, |c, o| c.ge(o)),
507                "<" => self.do_match::<T>(&c, |c, o| c.lt(o)),
508                "<=" => self.do_match::<T>(&c, |c, o| c.le(o)),
509                _ => {
510                    info!("unknown predicate {}", predicate);
511                    false
512                }
513            };
514        }
515        info!("user attr missing: {}", self.subject);
516        false
517    }
518
519    fn match_timestamp(&self, user: &FPUser, predicate: &str) -> bool {
520        let c: u128 = match user.get(&self.subject) {
521            Some(v) => match v.parse() {
522                Ok(v) => v,
523                Err(_) => return false,
524            },
525            None => unix_timestamp() / 1000,
526        };
527        match predicate {
528            "after" => self.do_match::<u128>(&c, |c, o| c.ge(o)),
529            "before" => self.do_match::<u128>(&c, |c, o| c.lt(o)),
530            _ => {
531                info!("unknown predicate {}", predicate);
532                false
533            }
534        }
535    }
536
537    fn do_match<T: FromStr>(&self, t: &T, f: fn(&T, &T) -> bool) -> bool {
538        self.objects
539            .iter()
540            .map(|o| match o.parse::<T>() {
541                Ok(o) => f(t, &o),
542                Err(_) => false,
543            })
544            .any(|x| x)
545    }
546
547    fn user_in_segments(&self, user: &FPUser, repo: &HashMap<String, Segment>) -> bool {
548        for segment_key in &self.objects {
549            match repo.get(segment_key) {
550                Some(segment) => {
551                    if segment.contains(user) {
552                        return true;
553                    }
554                }
555                None => warn!("segment not found {}", segment_key),
556            }
557        }
558        false
559    }
560}
561
562#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
563#[serde(rename_all = "camelCase")]
564pub struct Segment {
565    unique_id: String,
566    version: u64,
567    rules: Vec<SegmentRule>,
568}
569
570impl Segment {
571    pub fn contains(&self, user: &FPUser) -> bool {
572        for rule in &self.rules {
573            if rule.allow(user) {
574                return true;
575            }
576        }
577        false
578    }
579}
580
581#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
582#[serde(rename_all = "camelCase")]
583pub struct Repository {
584    pub segments: HashMap<String, Segment>,
585    pub toggles: HashMap<String, Toggle>,
586    pub events: Option<Value>,
587    // TODO: remove option next release
588    pub version: Option<u128>,
589    pub debug_until_time: Option<u64>,
590}
591
592impl Default for Repository {
593    fn default() -> Self {
594        Repository {
595            segments: Default::default(),
596            toggles: Default::default(),
597            events: Default::default(),
598            version: Some(0),
599            debug_until_time: None,
600        }
601    }
602}
603
604fn validate_toggle(_toggle: &Toggle) -> Result<(), FPError> {
605    //TODO: validate toggle segment unique id exists
606    //TODO: validate serve index and buckets size less than variations length
607    //TODO: validate rules list last one if default rule (no condition just serve)
608    //TODO: validate bucket is full range
609    Ok(())
610}
611
612#[allow(dead_code)]
613pub fn load_json(json_str: &str) -> Result<Repository, FPError> {
614    let repo = serde_json::from_str::<Repository>(json_str)
615        .map_err(|e| FPError::JsonError(json_str.to_owned(), e));
616    if let Ok(repo) = &repo {
617        for t in repo.toggles.values() {
618            validate_toggle(t)?
619        }
620    }
621    repo
622}
623
624fn concat_reason(reason1: String, reason2: Option<String>) -> String {
625    if let Some(reason2) = reason2 {
626        return format!("{reason1}. {reason2}.");
627    }
628    format!("{reason1}.")
629}
630
631#[cfg(test)]
632mod tests {
633    use super::*;
634    use approx::{self, assert_relative_eq};
635    use std::fs;
636    use std::path::PathBuf;
637
638    const MAX_DEEP: u8 = 20;
639
640    #[test]
641    fn test_load() {
642        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
643        path.push("resources/fixtures/repo.json");
644        let json_str = fs::read_to_string(path).unwrap();
645        let repo = load_json(&json_str);
646        assert!(repo.is_ok());
647    }
648
649    #[test]
650    fn test_load_invalid_json() {
651        let json_str = "{invalid_json}";
652        let repo = load_json(json_str);
653        assert!(repo.is_err());
654    }
655
656    #[test]
657    fn test_salt_hash() {
658        let bucket = salt_hash("key", "salt", 10000);
659        assert_eq!(2647, bucket);
660    }
661
662    #[test]
663    fn test_segment_condition() {
664        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
665        path.push("resources/fixtures/repo.json");
666        let json_str = fs::read_to_string(path).unwrap();
667        let repo = load_json(&json_str);
668        assert!(repo.is_ok());
669        let repo = repo.unwrap();
670
671        let user = FPUser::new().with("city", "4");
672        let toggle = repo.toggles.get("json_toggle").unwrap();
673        let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP, None);
674        let r = r.value.unwrap();
675        let r = r.as_object().unwrap();
676        assert!(r.get("variation_1").is_some());
677    }
678
679    #[test]
680    fn test_not_in_segment_condition() {
681        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
682        path.push("resources/fixtures/repo.json");
683        let json_str = fs::read_to_string(path).unwrap();
684        let repo = load_json(&json_str);
685        assert!(repo.is_ok());
686        let repo = repo.unwrap();
687
688        let user = FPUser::new().with("city", "100");
689        let toggle = repo.toggles.get("not_in_segment").unwrap();
690        let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP, None);
691        let r = r.value.unwrap();
692        let r = r.as_object().unwrap();
693        assert!(r.get("not_in").is_some());
694    }
695
696    #[test]
697    fn test_multi_condition() {
698        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
699        path.push("resources/fixtures/repo.json");
700        let json_str = fs::read_to_string(path).unwrap();
701        let repo = load_json(&json_str);
702        assert!(repo.is_ok());
703        let repo = repo.unwrap();
704
705        let user = FPUser::new().with("city", "1").with("os", "linux");
706        let toggle = repo.toggles.get("multi_condition_toggle").unwrap();
707        let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP, None);
708        let r = r.value.unwrap();
709        let r = r.as_object().unwrap();
710        assert!(r.get("variation_0").is_some());
711
712        let user = FPUser::new().with("os", "linux");
713        let toggle = repo.toggles.get("multi_condition_toggle").unwrap();
714        let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP, None);
715        assert!(r.reason.starts_with("default"));
716
717        let user = FPUser::new().with("city", "1");
718        let toggle = repo.toggles.get("multi_condition_toggle").unwrap();
719        let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP, None);
720        assert!(r.reason.starts_with("default"));
721    }
722
723    #[test]
724    fn test_distribution_condition() {
725        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
726        path.push("resources/fixtures/repo.json");
727        let json_str = fs::read_to_string(path).unwrap();
728        let repo = load_json(&json_str);
729        assert!(repo.is_ok());
730        let repo = repo.unwrap();
731
732        let total = 10000;
733        let users = gen_users(total, false);
734        let toggle = repo.toggles.get("json_toggle").unwrap();
735        let mut variation_0 = 0;
736        let mut variation_1 = 0;
737        let mut variation_2 = 0;
738        for user in &users {
739            let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP, None);
740            let r = r.value.unwrap();
741            let r = r.as_object().unwrap();
742            if r.get("variation_0").is_some() {
743                variation_0 += 1;
744            } else if r.get("variation_1").is_some() {
745                variation_1 += 1;
746            } else if r.get("variation_2").is_some() {
747                variation_2 += 1;
748            }
749        }
750
751        let rate0 = variation_0 as f64 / total as f64;
752        assert_relative_eq!(0.3333, rate0, max_relative = 0.05);
753        let rate1 = variation_1 as f64 / total as f64;
754        assert_relative_eq!(0.3333, rate1, max_relative = 0.05);
755        let rate2 = variation_2 as f64 / total as f64;
756        assert_relative_eq!(0.3333, rate2, max_relative = 0.05);
757    }
758
759    #[test]
760    fn test_disabled_toggle() {
761        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
762        path.push("resources/fixtures/repo.json");
763        let json_str = fs::read_to_string(path).unwrap();
764        let repo = load_json(&json_str);
765        assert!(repo.is_ok());
766        let repo = repo.unwrap();
767
768        let user = FPUser::new().with("city", "100");
769        let toggle = repo.toggles.get("disabled_toggle").unwrap();
770        let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP, None);
771        assert!(r
772            .value
773            .unwrap()
774            .as_object()
775            .unwrap()
776            .get("disabled_key")
777            .is_some());
778    }
779
780    #[test]
781    fn test_prerequisite_toggle() {
782        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
783        path.push("resources/fixtures/repo.json");
784        let json_str = fs::read_to_string(path).unwrap();
785        let repo = load_json(&json_str);
786        assert!(repo.is_ok());
787        let repo = repo.unwrap();
788
789        let user = FPUser::new().with("city", "4");
790
791        let toggle = repo.toggles.get("prerequisite_toggle").unwrap();
792        let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP, None);
793
794        assert!(r.value.unwrap().as_object().unwrap().get("2").is_some());
795    }
796
797    #[test]
798    fn test_prerequisite_not_exist_should_return_disabled_variation() {
799        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
800        path.push("resources/fixtures/repo.json");
801        let json_str = fs::read_to_string(path).unwrap();
802        let repo = load_json(&json_str);
803        assert!(repo.is_ok());
804        let repo = repo.unwrap();
805
806        let user = FPUser::new().with("city", "4");
807
808        let toggle = repo.toggles.get("prerequisite_toggle_not_exist").unwrap();
809        let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP, None);
810
811        assert!(r.value.unwrap().as_object().unwrap().get("0").is_some());
812        assert!(r.reason.contains("not exist"));
813    }
814
815    #[test]
816    fn test_prerequisite_not_match_should_return_disabled_variation() {
817        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
818        path.push("resources/fixtures/repo.json");
819        let json_str = fs::read_to_string(path).unwrap();
820        let repo = load_json(&json_str);
821        assert!(repo.is_ok());
822        let repo = repo.unwrap();
823
824        let user = FPUser::new().with("city", "4");
825
826        let toggle = repo.toggles.get("prerequisite_toggle_not_match").unwrap();
827        let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP, None);
828
829        assert!(r.value.unwrap().as_object().unwrap().get("0").is_some());
830        assert!(r.reason.contains("disabled."));
831    }
832
833    #[test]
834    fn test_prerequisite_depth_overflow_should_return_disabled_variation() {
835        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
836        path.push("resources/fixtures/repo.json");
837        let json_str = fs::read_to_string(path).unwrap();
838        let repo = load_json(&json_str);
839        assert!(repo.is_ok());
840        let repo = repo.unwrap();
841
842        let user = FPUser::new().with("city", "4");
843
844        let toggle = repo.toggles.get("prerequisite_toggle").unwrap();
845        let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, 1, None);
846
847        assert!(r.value.unwrap().as_object().unwrap().get("0").is_some());
848        assert!(r.reason.contains("depth overflow"));
849    }
850
851    fn gen_users(num: usize, random: bool) -> Vec<FPUser> {
852        let mut users = Vec::with_capacity(num);
853        for i in 0..num {
854            let key: u64 = if random { rand::random() } else { i as u64 };
855            let u = FPUser::new()
856                .with("city", "100")
857                .stable_rollout(format!("{}", key));
858            users.push(u);
859        }
860        users
861    }
862}
863
864#[cfg(test)]
865mod distribution_tests {
866    use super::*;
867
868    #[test]
869    fn test_distribution_in_exact_bucket() {
870        let distribution = Distribution {
871            distribution: vec![
872                vec![BucketRange((0, 2647))],
873                vec![BucketRange((2647, 2648))],
874                vec![BucketRange((2648, 10000))],
875            ],
876            bucket_by: Some("name".to_string()),
877            salt: Some("salt".to_string()),
878        };
879
880        let user_bucket_by_name = FPUser::new().with("name", "key");
881
882        let params = EvalParams {
883            key: "not care",
884            is_detail: true,
885            user: &user_bucket_by_name,
886            variations: &[],
887            segment_repo: &Default::default(),
888            toggle_repo: &Default::default(),
889            debug_until_time: None,
890        };
891        let result = distribution.find_index(&params);
892
893        assert_eq!(1, result.unwrap_or_default());
894    }
895
896    #[test]
897    fn test_distribution_in_none_bucket() {
898        let distribution = Distribution {
899            distribution: vec![
900                vec![BucketRange((0, 2647))],
901                vec![BucketRange((2648, 10000))],
902            ],
903            bucket_by: Some("name".to_string()),
904            salt: Some("salt".to_string()),
905        };
906
907        let user_bucket_by_name = FPUser::new().with("name", "key");
908
909        let params = EvalParams {
910            key: "not care",
911            is_detail: true,
912            user: &user_bucket_by_name,
913            variations: &[],
914            segment_repo: &Default::default(),
915            toggle_repo: &Default::default(),
916            debug_until_time: None,
917        };
918        let result = distribution.find_index(&params);
919
920        assert!(format!("{:?}", result.expect_err("error")).contains("not find hash_bucket"));
921
922        let params_no_detail = EvalParams {
923            key: "not care",
924            is_detail: false,
925            user: &user_bucket_by_name,
926            variations: &[],
927            segment_repo: &Default::default(),
928            toggle_repo: &Default::default(),
929            debug_until_time: None,
930        };
931        let result = distribution.find_index(&params_no_detail);
932        assert!(result.is_err());
933    }
934
935    #[test]
936    fn test_select_variation_fail() {
937        let distribution = Distribution {
938            distribution: vec![
939                vec![BucketRange((0, 5000))],
940                vec![BucketRange((5000, 10000))],
941            ],
942            bucket_by: Some("name".to_string()),
943            salt: Some("salt".to_string()),
944        };
945        let serve = Serve::Split(distribution);
946
947        let user_with_no_name = FPUser::new();
948
949        let params = EvalParams {
950            key: "",
951            is_detail: true,
952            user: &user_with_no_name,
953            variations: &[
954                Value::String("a".to_string()),
955                Value::String("b".to_string()),
956            ],
957            segment_repo: &Default::default(),
958            toggle_repo: &Default::default(),
959            debug_until_time: None,
960        };
961
962        let result = serve.select_variation(&params).expect_err("e");
963
964        assert!(format!("{:?}", result).contains("does not have attribute"));
965    }
966}
967
968#[cfg(test)]
969mod condition_tests {
970    use super::*;
971    use std::fs;
972    use std::path::PathBuf;
973
974    const MAX_DEEP: u8 = 20;
975
976    #[test]
977    fn test_unknown_condition() {
978        let json_str = r#"
979        {
980            "type": "new_type",
981            "subject": "new_subject",
982            "predicate": ">",
983            "objects": []
984        }
985        "#;
986
987        let condition = serde_json::from_str::<Condition>(json_str);
988        assert!(condition.is_ok());
989        let condition = condition.unwrap();
990        assert_eq!(condition.r#type, ConditionType::Unknown);
991    }
992
993    #[test]
994    fn test_match_is_one_of() {
995        let condition = Condition {
996            r#type: ConditionType::String,
997            subject: "name".to_string(),
998            predicate: "is one of".to_string(),
999            objects: vec![String::from("hello"), String::from("world")],
1000        };
1001
1002        let user = FPUser::new().with("name", "world");
1003        assert!(condition.match_string(&user, &condition.predicate));
1004    }
1005
1006    #[test]
1007    fn test_not_match_is_one_of() {
1008        let condition = Condition {
1009            r#type: ConditionType::String,
1010            subject: "name".to_string(),
1011            predicate: "is one of".to_string(),
1012            objects: vec![String::from("hello"), String::from("world")],
1013        };
1014
1015        let user = FPUser::new().with("name", "not_in");
1016
1017        assert!(!condition.match_string(&user, &condition.predicate));
1018    }
1019
1020    #[test]
1021    fn test_user_miss_key_is_not_one_of() {
1022        let condition = Condition {
1023            r#type: ConditionType::String,
1024            subject: "name".to_string(),
1025            predicate: "is not one of".to_string(),
1026            objects: vec![String::from("hello"), String::from("world")],
1027        };
1028
1029        let user = FPUser::new();
1030
1031        assert!(!condition.match_string(&user, &condition.predicate));
1032    }
1033
1034    #[test]
1035    fn test_match_is_not_any_of() {
1036        let condition = Condition {
1037            r#type: ConditionType::String,
1038            subject: "name".to_string(),
1039            predicate: "is not any of".to_string(),
1040            objects: vec![String::from("hello"), String::from("world")],
1041        };
1042
1043        let user = FPUser::new().with("name", "welcome");
1044        assert!(condition.match_string(&user, &condition.predicate));
1045    }
1046
1047    #[test]
1048    fn test_not_match_is_not_any_of() {
1049        let condition = Condition {
1050            r#type: ConditionType::String,
1051            subject: "name".to_string(),
1052            predicate: "is not any of".to_string(),
1053            objects: vec![String::from("hello"), String::from("world")],
1054        };
1055
1056        let user = FPUser::new().with("name", "not_in");
1057
1058        assert!(condition.match_string(&user, &condition.predicate));
1059    }
1060
1061    #[test]
1062    fn test_match_ends_with() {
1063        let condition = Condition {
1064            r#type: ConditionType::String,
1065            subject: "name".to_string(),
1066            predicate: "ends with".to_string(),
1067            objects: vec![String::from("hello"), String::from("world")],
1068        };
1069
1070        let user = FPUser::new().with("name", "bob world");
1071
1072        assert!(condition.match_string(&user, &condition.predicate));
1073    }
1074
1075    #[test]
1076    fn test_dont_match_ends_with() {
1077        let condition = Condition {
1078            r#type: ConditionType::String,
1079            subject: "name".to_string(),
1080            predicate: "ends with".to_string(),
1081            objects: vec![String::from("hello"), String::from("world")],
1082        };
1083
1084        let user = FPUser::new().with("name", "bob");
1085
1086        assert!(!condition.match_string(&user, &condition.predicate));
1087    }
1088
1089    #[test]
1090    fn test_match_does_not_end_with() {
1091        let condition = Condition {
1092            r#type: ConditionType::String,
1093            subject: "name".to_string(),
1094            predicate: "does not end with".to_string(),
1095            objects: vec![String::from("hello"), String::from("world")],
1096        };
1097
1098        let user = FPUser::new().with("name", "bob");
1099
1100        assert!(condition.match_string(&user, &condition.predicate));
1101    }
1102
1103    #[test]
1104    fn test_not_match_does_not_end_with() {
1105        let condition = Condition {
1106            r#type: ConditionType::String,
1107            subject: "name".to_string(),
1108            predicate: "does not end with".to_string(),
1109            objects: vec![String::from("hello"), String::from("world")],
1110        };
1111
1112        let user = FPUser::new().with("name", "bob world");
1113
1114        assert!(!condition.match_string(&user, &condition.predicate));
1115    }
1116
1117    #[test]
1118    fn test_match_starts_with() {
1119        let condition = Condition {
1120            r#type: ConditionType::String,
1121            subject: "name".to_string(),
1122            predicate: "starts with".to_string(),
1123            objects: vec![String::from("hello"), String::from("world")],
1124        };
1125
1126        let user = FPUser::new().with("name", "world bob");
1127
1128        assert!(condition.match_string(&user, &condition.predicate));
1129    }
1130
1131    #[test]
1132    fn test_not_match_starts_with() {
1133        let condition = Condition {
1134            r#type: ConditionType::String,
1135            subject: "name".to_string(),
1136            predicate: "ends with".to_string(),
1137            objects: vec![String::from("hello"), String::from("world")],
1138        };
1139
1140        let user = FPUser::new().with("name", "bob");
1141
1142        assert!(!condition.match_string(&user, &condition.predicate));
1143    }
1144
1145    #[test]
1146    fn test_match_does_not_start_with() {
1147        let condition = Condition {
1148            r#type: ConditionType::String,
1149            subject: "name".to_string(),
1150            predicate: "does not start with".to_string(),
1151            objects: vec![String::from("hello"), String::from("world")],
1152        };
1153
1154        let user = FPUser::new().with("name", "bob");
1155
1156        assert!(condition.match_string(&user, &condition.predicate));
1157    }
1158
1159    #[test]
1160    fn test_not_match_does_not_start_with() {
1161        let condition = Condition {
1162            r#type: ConditionType::String,
1163            subject: "name".to_string(),
1164            predicate: "does not start with".to_string(),
1165            objects: vec![String::from("hello"), String::from("world")],
1166        };
1167
1168        let user = FPUser::new().with("name", "world bob");
1169
1170        assert!(!condition.match_string(&user, &condition.predicate));
1171    }
1172
1173    #[test]
1174    fn test_match_contains() {
1175        let condition = Condition {
1176            r#type: ConditionType::String,
1177            subject: "name".to_string(),
1178            predicate: "contains".to_string(),
1179            objects: vec![String::from("hello"), String::from("world")],
1180        };
1181
1182        let user = FPUser::new().with("name", "alice world bob");
1183
1184        assert!(condition.match_string(&user, &condition.predicate));
1185    }
1186
1187    #[test]
1188    fn test_not_match_contains() {
1189        let condition = Condition {
1190            r#type: ConditionType::String,
1191            subject: "name".to_string(),
1192            predicate: "contains".to_string(),
1193            objects: vec![String::from("hello"), String::from("world")],
1194        };
1195
1196        let user = FPUser::new().with("name", "alice bob");
1197
1198        assert!(!condition.match_string(&user, &condition.predicate));
1199    }
1200
1201    #[test]
1202    fn test_match_not_contains() {
1203        let condition = Condition {
1204            r#type: ConditionType::String,
1205            subject: "name".to_string(),
1206            predicate: "does not contain".to_string(),
1207            objects: vec![String::from("hello"), String::from("world")],
1208        };
1209
1210        let user = FPUser::new().with("name", "alice bob");
1211
1212        assert!(condition.match_string(&user, &condition.predicate));
1213    }
1214
1215    #[test]
1216    fn test_not_match_not_contains() {
1217        let condition = Condition {
1218            r#type: ConditionType::String,
1219            subject: "name".to_string(),
1220            predicate: "does not contain".to_string(),
1221            objects: vec![String::from("hello"), String::from("world")],
1222        };
1223
1224        let user = FPUser::new().with("name", "alice world bob");
1225
1226        assert!(!condition.match_string(&user, &condition.predicate));
1227    }
1228
1229    #[test]
1230    fn test_match_regex() {
1231        let condition = Condition {
1232            r#type: ConditionType::String,
1233            subject: "name".to_string(),
1234            predicate: "matches regex".to_string(),
1235            objects: vec![String::from("hello"), String::from("world.*")],
1236        };
1237
1238        let user = FPUser::new().with("name", "alice world bob");
1239
1240        assert!(condition.match_string(&user, &condition.predicate));
1241    }
1242
1243    #[test]
1244    fn test_match_regex_first_object() {
1245        let condition = Condition {
1246            r#type: ConditionType::String,
1247            subject: "name".to_string(),
1248            predicate: "matches regex".to_string(),
1249            objects: vec![String::from(r"hello\d"), String::from("world.*")],
1250        };
1251
1252        let user = FPUser::new().with("name", "alice orld bob hello3");
1253
1254        assert!(condition.match_string(&user, &condition.predicate));
1255    }
1256
1257    #[test]
1258    fn test_not_match_regex() {
1259        let condition = Condition {
1260            r#type: ConditionType::String,
1261            subject: "name".to_string(),
1262            predicate: "matches regex".to_string(),
1263            objects: vec![String::from(r"hello\d"), String::from("world.*")],
1264        };
1265
1266        let user = FPUser::new().with("name", "alice orld bob hello");
1267
1268        assert!(!condition.match_string(&user, &condition.predicate));
1269    }
1270
1271    #[test]
1272    fn test_match_not_match_regex() {
1273        let condition = Condition {
1274            r#type: ConditionType::String,
1275            subject: "name".to_string(),
1276            predicate: "does not match regex".to_string(),
1277            objects: vec![String::from(r"hello\d"), String::from("world.*")],
1278        };
1279
1280        let user = FPUser::new().with("name", "alice orld bob hello");
1281
1282        assert!(condition.match_string(&user, &condition.predicate));
1283    }
1284
1285    #[test]
1286    fn test_invalid_regex_condition() {
1287        let condition = Condition {
1288            r#type: ConditionType::String,
1289            subject: "name".to_string(),
1290            predicate: "matches regex".to_string(),
1291            objects: vec![String::from("\\\\\\")],
1292        };
1293
1294        let user = FPUser::new().with("name", "\\\\\\");
1295
1296        assert!(!condition.match_string(&user, &condition.predicate));
1297    }
1298
1299    #[test]
1300    fn test_match_equal_string() {
1301        let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
1302        path.push("resources/fixtures/repo.json");
1303        let json_str = fs::read_to_string(path).unwrap();
1304        let repo = load_json(&json_str);
1305        assert!(repo.is_ok());
1306        let repo = repo.unwrap();
1307
1308        let user = FPUser::new().with("city", "1");
1309        let toggle = repo.toggles.get("json_toggle").unwrap();
1310        let r = toggle.eval(&user, &repo.segments, &repo.toggles, false, MAX_DEEP, None);
1311        let r = r.value.unwrap();
1312        let r = r.as_object().unwrap();
1313        assert!(r.get("variation_0").is_some());
1314    }
1315
1316    #[test]
1317    fn test_segment_deserialize() {
1318        let json_str = r#"
1319        {
1320            "type":"segment",
1321            "predicate":"is in",
1322            "objects":[ "segment1","segment2"]
1323        }
1324        "#;
1325
1326        let segment = serde_json::from_str::<Condition>(json_str)
1327            .map_err(|e| FPError::JsonError(json_str.to_owned(), e));
1328        assert!(segment.is_ok())
1329    }
1330
1331    #[test]
1332    fn test_semver_condition() {
1333        let mut condition = Condition {
1334            r#type: ConditionType::Semver,
1335            subject: "version".to_owned(),
1336            objects: vec!["1.0.0".to_owned(), "2.0.0".to_owned()],
1337            predicate: "=".to_owned(),
1338        };
1339
1340        let user = FPUser::new().with("version".to_owned(), "1.0.0".to_owned());
1341        assert!(condition.meet(&user, None));
1342        let user = FPUser::new().with("version".to_owned(), "2.0.0".to_owned());
1343        assert!(condition.meet(&user, None));
1344        let user = FPUser::new().with("version".to_owned(), "3.0.0".to_owned());
1345        assert!(!condition.meet(&user, None));
1346
1347        condition.predicate = "!=".to_owned();
1348        let user = FPUser::new().with("version".to_owned(), "1.0.0".to_owned());
1349        assert!(!condition.meet(&user, None));
1350        let user = FPUser::new().with("version".to_owned(), "2.0.0".to_owned());
1351        assert!(!condition.meet(&user, None));
1352        let user = FPUser::new().with("version".to_owned(), "0.1.0".to_owned());
1353        assert!(condition.meet(&user, None));
1354
1355        condition.predicate = ">".to_owned();
1356        let user = FPUser::new().with("version".to_owned(), "2.0.0".to_owned());
1357        assert!(condition.meet(&user, None));
1358        let user = FPUser::new().with("version".to_owned(), "3.0.0".to_owned());
1359        assert!(condition.meet(&user, None));
1360        let user = FPUser::new().with("version".to_owned(), "0.1.0".to_owned());
1361        assert!(!condition.meet(&user, None));
1362
1363        condition.predicate = ">=".to_owned();
1364        let user = FPUser::new().with("version".to_owned(), "1.0.0".to_owned());
1365        assert!(condition.meet(&user, None));
1366        let user = FPUser::new().with("version".to_owned(), "2.0.0".to_owned());
1367        assert!(condition.meet(&user, None));
1368        let user = FPUser::new().with("version".to_owned(), "3.0.0".to_owned());
1369        assert!(condition.meet(&user, None));
1370        let user = FPUser::new().with("version".to_owned(), "0.1.0".to_owned());
1371        assert!(!condition.meet(&user, None));
1372
1373        condition.predicate = "<".to_owned();
1374        let user = FPUser::new().with("version".to_owned(), "1.0.0".to_owned()); // < 2.0.0
1375        assert!(condition.meet(&user, None));
1376        let user = FPUser::new().with("version".to_owned(), "2.0.0".to_owned());
1377        assert!(!condition.meet(&user, None));
1378        let user = FPUser::new().with("version".to_owned(), "3.0.0".to_owned());
1379        assert!(!condition.meet(&user, None));
1380
1381        condition.predicate = "<=".to_owned();
1382        let user = FPUser::new().with("version".to_owned(), "1.0.0".to_owned());
1383        assert!(condition.meet(&user, None));
1384        let user = FPUser::new().with("version".to_owned(), "2.0.0".to_owned());
1385        assert!(condition.meet(&user, None));
1386        let user = FPUser::new().with("version".to_owned(), "0.1.0".to_owned());
1387        assert!(condition.meet(&user, None));
1388
1389        let user = FPUser::new().with("version".to_owned(), "a".to_owned());
1390        assert!(!condition.meet(&user, None));
1391    }
1392
1393    #[test]
1394    fn test_number_condition() {
1395        let mut condition = Condition {
1396            r#type: ConditionType::Number,
1397            subject: "price".to_owned(),
1398            objects: vec!["10".to_owned(), "100".to_owned()],
1399            predicate: "=".to_owned(),
1400        };
1401
1402        let user = FPUser::new().with("price".to_owned(), "10".to_owned());
1403        assert!(condition.meet(&user, None));
1404        let user = FPUser::new().with("price".to_owned(), "100".to_owned());
1405        assert!(condition.meet(&user, None));
1406        let user = FPUser::new().with("price".to_owned(), "0".to_owned());
1407        assert!(!condition.meet(&user, None));
1408
1409        condition.predicate = "!=".to_owned();
1410        let user = FPUser::new().with("price".to_owned(), "10".to_owned());
1411        assert!(!condition.meet(&user, None));
1412        let user = FPUser::new().with("price".to_owned(), "100".to_owned());
1413        assert!(!condition.meet(&user, None));
1414        let user = FPUser::new().with("price".to_owned(), "0".to_owned());
1415        assert!(condition.meet(&user, None));
1416
1417        condition.predicate = ">".to_owned();
1418        let user = FPUser::new().with("price".to_owned(), "11".to_owned());
1419        assert!(condition.meet(&user, None));
1420        let user = FPUser::new().with("price".to_owned(), "10".to_owned());
1421        assert!(!condition.meet(&user, None));
1422
1423        condition.predicate = ">=".to_owned();
1424        let user = FPUser::new().with("price".to_owned(), "10".to_owned());
1425        assert!(condition.meet(&user, None));
1426        let user = FPUser::new().with("price".to_owned(), "11".to_owned());
1427        assert!(condition.meet(&user, None));
1428        let user = FPUser::new().with("price".to_owned(), "100".to_owned());
1429        assert!(condition.meet(&user, None));
1430        let user = FPUser::new().with("price".to_owned(), "0".to_owned());
1431        assert!(!condition.meet(&user, None));
1432
1433        condition.predicate = "<".to_owned();
1434        let user = FPUser::new().with("price".to_owned(), "1".to_owned());
1435        assert!(condition.meet(&user, None));
1436        let user = FPUser::new().with("price".to_owned(), "10".to_owned()); // < 100
1437        assert!(condition.meet(&user, None));
1438        let user = FPUser::new().with("price".to_owned(), "100".to_owned()); // < 100
1439        assert!(!condition.meet(&user, None));
1440
1441        condition.predicate = "<=".to_owned();
1442        let user = FPUser::new().with("price".to_owned(), "1".to_owned());
1443        assert!(condition.meet(&user, None));
1444        let user = FPUser::new().with("price".to_owned(), "10".to_owned()); // < 100
1445        assert!(condition.meet(&user, None));
1446        let user = FPUser::new().with("price".to_owned(), "100".to_owned()); // < 100
1447        assert!(condition.meet(&user, None));
1448
1449        let user = FPUser::new().with("price".to_owned(), "a".to_owned());
1450        assert!(!condition.meet(&user, None));
1451    }
1452
1453    #[test]
1454    fn test_datetime_condition() {
1455        let now_ts = unix_timestamp() / 1000;
1456        let mut condition = Condition {
1457            r#type: ConditionType::Datetime,
1458            subject: "ts".to_owned(),
1459            objects: vec![format!("{}", now_ts)],
1460            predicate: "after".to_owned(),
1461        };
1462
1463        let user = FPUser::new();
1464        assert!(condition.meet(&user, None));
1465        let user = FPUser::new().with("ts".to_owned(), format!("{}", now_ts));
1466        assert!(condition.meet(&user, None));
1467
1468        condition.predicate = "before".to_owned();
1469        condition.objects = vec![format!("{}", now_ts + 2)];
1470        assert!(condition.meet(&user, None));
1471
1472        let user = FPUser::new().with("ts".to_owned(), "a".to_owned());
1473        assert!(!condition.meet(&user, None));
1474    }
1475}