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, })
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 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 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(¶ms);
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(¶ms);
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(¶ms_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(¶ms).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()); 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()); assert!(condition.meet(&user, None));
1438 let user = FPUser::new().with("price".to_owned(), "100".to_owned()); 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()); assert!(condition.meet(&user, None));
1446 let user = FPUser::new().with("price".to_owned(), "100".to_owned()); 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}