use clean_insights_sdk::campaign::{Campaign, EventAggregationRule}; use clean_insights_sdk::cleaninsights::CleanInsights; use clean_insights_sdk::configuration::Configuration; use clean_insights_sdk::consents::{Feature, Consent, Consents, ConsentState}; use clean_insights_sdk::default_store::DefaultStore; use clean_insights_sdk::event::Event; use clean_insights_sdk::insights::Insights; use clean_insights_sdk::store::Store; use clean_insights_sdk::visit::Visit; use std::collections::HashMap; use std::error::Error; use std::fs::{remove_file, File}; use std::io::BufReader; use std::mem::transmute; use std::path::Path; use serial_test::serial; use chrono::{DateTime, Utc, Duration, Timelike}; struct Tests { conf: Configuration, ci: CleanInsights, } impl Tests { const STORE_FILE: &'static str = "tests/cleaninsights.json"; fn setup(strengthen_anonymity: bool, max_age_of_old_data: Option) -> Tests { remove_file(Path::new(Tests::STORE_FILE)).unwrap_or(()); let mut campaigns = HashMap::new(); campaigns.insert("test".to_string(), Campaign { start: DateTime::from(DateTime::parse_from_rfc3339("2021-01-01T00:00:00-00:00").unwrap()), end: DateTime::from(DateTime::parse_from_rfc3339("2099-12-31T23:59:59-00:00").unwrap()), aggregation_period_length: 1, number_of_periods: 90, only_record_once: false, event_aggregation_rule: EventAggregationRule::Avg, strengthen_anonymity }); let conf = Configuration { server: "http://localhost:8080/ci/cleaninsights.php".to_string(), site_id: 1, timeout: 1, max_retry_delay: 1, max_age_of_old_data: max_age_of_old_data.unwrap_or(1), persist_every_n_times: 1, server_side_anonymous_usage: false, debug: true, campaigns }; let ci = if strengthen_anonymity { CleanInsights::new_with_default_store(conf.clone(), "tests") } else { CleanInsights::new_from_json_with_default_store("tests/testconf.json", "tests") }; Tests { conf, ci } } fn stored_store(&self) -> DefaultStore { let file = File::open(Path::new(Tests::STORE_FILE)).unwrap(); let reader = BufReader::new(file); serde_json::from_reader(reader).unwrap() } fn fake_yesterday_consent(&self) { let mut store = self.stored_store(); store.consents_mut().campaigns.insert("test".to_string(), Consent { granted: true, start: Utc::now() - Duration::days(1), end: Utc::now() + Duration::days(2) }); let file = File::create(Tests::STORE_FILE).unwrap(); serde_json::to_writer(file, &store).unwrap(); } } #[allow(dead_code)] pub struct CleanInsightsTest { pub conf: Configuration, /// User languages in order of preference. pub lang: Vec, /// User Agent string. pub ua: Option, pub store: Box, persistence_counter: u32, sending: bool, failed_submission_count: u32, last_failed_submission: DateTime, } #[test] fn conf() { let t = Tests::setup(false, None); assert_eq!(t.conf, t.ci.conf); } #[test] fn deny_consent() { let mut t = Tests::setup(false, None); assert_eq!(t.ci.state_of_feature(Feature::Lang), ConsentState::Unknown); assert_eq!(t.ci.state_of_feature(Feature::Ua), ConsentState::Unknown); assert_eq!(t.ci.state_of_campaign("test"), ConsentState::Unknown); t.ci.deny_feature(Feature::Lang); t.ci.deny_feature(Feature::Ua); t.ci.deny_campaign("test"); assert_eq!(t.ci.campaign_consents_len(), 1); assert_eq!(t.ci.feature_consents_len(), 2); assert!(t.ci.get_feature_consent_by_index(0).is_some()); assert!(!t.ci.get_feature_consent_by_index(0).unwrap().granted); assert_eq!(t.ci.state_of_feature(Feature::Lang), ConsentState::Denied); assert!(t.ci.get_feature_consent_by_index(1).is_some()); assert!(!t.ci.get_feature_consent_by_index(1).unwrap().granted); assert_eq!(t.ci.state_of_feature(Feature::Ua), ConsentState::Denied); assert!(t.ci.get_feature_consent_by_index(2).is_none()); assert!(t.ci.get_campaign_consent_by_index(0).is_some()); assert!(!t.ci.get_campaign_consent_by_index(0).unwrap().granted); assert!(t.ci.get_campaign_consent_by_index(1).is_none()); assert!(!t.ci.is_campaign_currently_granted("test")); assert_eq!(t.ci.state_of_campaign("test"), ConsentState::Denied); } #[test] fn grant_consent() { let mut t = Tests::setup(false, None); assert_eq!(t.ci.state_of_feature(Feature::Lang), ConsentState::Unknown); assert_eq!(t.ci.state_of_feature(Feature::Ua), ConsentState::Unknown); assert_eq!(t.ci.state_of_campaign("test"), ConsentState::Unknown); t.ci.grant_feature(Feature::Lang); t.ci.grant_feature(Feature::Ua); t.ci.grant_campaign("test"); assert_eq!(t.ci.campaign_consents_len(), 1); assert_eq!(t.ci.feature_consents_len(), 2); assert!(t.ci.get_feature_consent_by_index(0).is_some()); assert!(t.ci.get_feature_consent_by_index(0).unwrap().granted); assert_eq!(t.ci.state_of_feature(Feature::Lang), ConsentState::Granted); assert!(t.ci.get_feature_consent_by_index(1).is_some()); assert!(t.ci.get_feature_consent_by_index(1).unwrap().granted); assert_eq!(t.ci.state_of_feature(Feature::Ua), ConsentState::Granted); assert!(t.ci.get_feature_consent_by_index(2).is_none()); assert!(t.ci.get_campaign_consent_by_index(0).is_some()); assert!(t.ci.get_campaign_consent_by_index(0).unwrap().granted); assert!(t.ci.get_campaign_consent_by_index(1).is_none()); assert!(t.ci.is_campaign_currently_granted("test")); assert_eq!(t.ci.state_of_campaign("test"), ConsentState::Granted); } #[test] fn grant_consent_strengthened_anonymity() { let mut t = Tests::setup(true, None); assert_eq!(t.ci.state_of_feature(Feature::Lang), ConsentState::Unknown); assert_eq!(t.ci.state_of_feature(Feature::Ua), ConsentState::Unknown); assert_eq!(t.ci.state_of_campaign("test"), ConsentState::Unknown); t.ci.grant_feature(Feature::Lang); t.ci.grant_feature(Feature::Ua); t.ci.grant_campaign("test"); assert_eq!(t.ci.campaign_consents_len(), 1); assert_eq!(t.ci.feature_consents_len(), 2); assert!(t.ci.get_feature_consent_by_index(0).is_some()); assert!(t.ci.get_feature_consent_by_index(0).unwrap().granted); assert_eq!(t.ci.state_of_feature(Feature::Lang), ConsentState::Granted); assert!(t.ci.get_feature_consent_by_index(1).is_some()); assert!(t.ci.get_feature_consent_by_index(1).unwrap().granted); assert_eq!(t.ci.state_of_feature(Feature::Ua), ConsentState::Granted); assert!(t.ci.get_feature_consent_by_index(2).is_none()); assert!(t.ci.get_campaign_consent_by_index(0).is_some()); assert!(t.ci.get_campaign_consent_by_index(0).unwrap().granted); assert!(t.ci.get_campaign_consent_by_index(1).is_none()); // Should only ever allowed on the start of the next measurement period, // which should be tomorrow, as by the tested configuration. assert!(!t.ci.is_campaign_currently_granted("test")); assert_eq!(t.ci.state_of_campaign("test"), ConsentState::NotStarted); } #[test] fn invalid_campaign() { let mut t = Tests::setup(false, None); assert!(t.ci.consent_for_campaign("foobar").is_none()); assert_eq!(t.ci.state_of_campaign("foobar"), ConsentState::Unconfigured); assert!(t.ci.can_ask_consent_for_campaign("foobar").is_none()); assert!(t.ci.grant_campaign("foobar").is_none()); assert!(t.ci.deny_campaign("foobar").is_none()); assert_eq!(t.ci.campaign_consents_len(), 0); assert!(!t.ci.is_campaign_currently_granted("foobar")) } #[test] #[serial] fn persistence() { let mut t = Tests::setup(false, None); t.ci.grant_feature(Feature::Lang); t.ci.grant_feature(Feature::Ua); t.ci.grant_campaign("test"); t.ci.measure_visit(&["foo"], "test"); t.ci.measure_event("foo", "bar", "test", Some("baz".to_string()), Some(4567.0)); t.ci.persist(); let store = t.stored_store(); unsafe { let cit: &CleanInsightsTest = transmute(&t.ci); assert_eq!(store.consents(), cit.store.consents()); }; assert!(t.ci.is_campaign_currently_granted("test")); t.ci.measure_visit(&["foo"], "test"); t.ci.measure_event("foo", "bar", "test", Some("baz".to_string()), Some(4567.0)); t.ci.persist(); let store = t.stored_store(); let midnight = Utc::now().with_hour(0).unwrap() .with_minute(0).unwrap() .with_second(0).unwrap() .with_nanosecond(0).unwrap(); let midnight_tomorrow = midnight + Duration::days(1); let v: Vec = vec![Visit { scene_path: vec!["foo".to_string()], campaign_id: "test".to_string(), times: 2, first: midnight, last: midnight_tomorrow }]; assert_eq!(store.visits(), &v); let e: Vec = vec![Event { category: "foo".to_string(), action: "bar".to_string(), name: Some("baz".to_string()), value: Some(4567.0), campaign_id: "test".to_string(), times: 2, first: midnight, last: midnight_tomorrow }]; assert_eq!(store.events(), &e); } #[test] #[serial] fn persistence_strengthened_anonymity() { let mut t = Tests::setup(true, None); t.ci.grant_feature(Feature::Lang); t.ci.grant_feature(Feature::Ua); t.ci.grant_campaign("test"); t.ci.measure_visit(&["foo"], "test"); t.ci.measure_event("foo", "bar", "test", Some("baz".to_string()), Some(4567.0)); t.ci.persist(); let store = t.stored_store(); unsafe { let cit: &CleanInsightsTest = transmute(&t.ci); assert_eq!(store.consents(), cit.store.consents()); }; // Consent will only start tomorrow! let v: Vec = vec![]; assert_eq!(store.visits(), &v); let e: Vec = vec![]; assert_eq!(store.events(), &e); t.fake_yesterday_consent(); // Re-init with faked store. t.ci = CleanInsights::new_from_json_with_default_store("tests/testconf.json", "tests"); assert!(t.ci.is_campaign_currently_granted("test")); t.ci.measure_visit(&["foo"], "test"); t.ci.measure_event("foo", "bar", "test", Some("baz".to_string()), Some(4567.0)); t.ci.persist(); let store = t.stored_store(); let midnight = Utc::now().with_hour(0).unwrap() .with_minute(0).unwrap() .with_second(0).unwrap() .with_nanosecond(0).unwrap(); let midnight_tomorrow = midnight + Duration::days(1); let v: Vec = vec![Visit { scene_path: vec!["foo".to_string()], campaign_id: "test".to_string(), times: 1, first: midnight, last: midnight_tomorrow }]; assert_eq!(store.visits(), &v); let e: Vec = vec![Event { category: "foo".to_string(), action: "bar".to_string(), name: Some("baz".to_string()), value: Some(4567.0), campaign_id: "test".to_string(), times: 1, first: midnight, last: midnight_tomorrow }]; assert_eq!(store.events(), &e); } #[test] fn serialize_insights() { let mut insights = Insights { idsite: 1, lang: Some("foo".to_string()), ua: Some("bar".to_string()), visits: vec![], events: vec![] }; assert_eq!(serde_json::to_string(&insights).unwrap(), "{\"idsite\":1,\"lang\":\"foo\",\"ua\":\"bar\",\"visits\":[],\"events\":[]}"); let mut v: Vec = vec![Visit { scene_path: vec!["foo".to_string(), "bar".to_string()], campaign_id: "test".to_string(), times: 1, first: DateTime::parse_from_rfc3339("2022-01-01T00:00:00-00:00").unwrap().with_timezone(&Utc), last: DateTime::parse_from_rfc3339("2022-01-02T00:00:00-00:00").unwrap().with_timezone(&Utc) }]; let mut e: Vec = vec![Event { category: "foo".to_string(), action: "bar".to_string(), name: Some("baz".to_string()), value: Some(6.66), campaign_id: "test".to_string(), times: 1, first: DateTime::parse_from_rfc3339("2022-01-01T00:00:00-00:00").unwrap().with_timezone(&Utc), last: DateTime::parse_from_rfc3339("2022-01-02T00:00:00-00:00").unwrap().with_timezone(&Utc) }]; let mut store: Box = Box::new(TestStore::new()); store.visits_mut().append(&mut v); store.events_mut().append(&mut e); let t = Tests::setup(false, Some(30000)); insights = Insights::new(&t.conf, &mut *store, &vec![], &None); assert_eq!(serde_json::to_string(&insights).unwrap(), "{\"idsite\":1,\"visits\":[{\"action_name\":\"foo/bar\",\"campaign_id\":\"test\",\"times\":1,\"period_start\":1640995200,\"period_end\":1641081600}],\"events\":[{\"category\":\"foo\",\"action\":\"bar\",\"name\":\"baz\",\"value\":6.66,\"campaign_id\":\"test\",\"times\":1,\"period_start\":1640995200,\"period_end\":1641081600}]}") } #[test] fn default_store_send() { let store = DefaultStore::new( "/tmp", |message| debug(&message)); assert!(store.send("".to_string(), &"http://example.org/test".to_string(), 1.5 as u64).is_err()); } fn debug(message: &str) { eprintln!("[CleanInsightsSDK Test] {}", message); } #[derive(Debug)] struct TestStore { consents: Consents, visits: Vec, events: Vec, } impl TestStore { fn new() -> TestStore { TestStore { consents: Consents { features: Default::default(), campaigns: Default::default() }, visits: vec![], events: vec![] } } } impl Store for TestStore { fn consents(&self) -> &Consents { &self.consents } fn consents_mut(&mut self) -> &mut Consents { &mut self.consents } fn visits(&self) -> &Vec { &self.visits } fn visits_mut(&mut self) -> &mut Vec { &mut self.visits } fn events(&self) -> &Vec { &self.events } fn events_mut(&mut self) -> &mut Vec { &mut self.events } fn persist(&self) -> Result<(), Box> { Ok(()) } fn send(&self, _data: String, _server: &String, _timeout: u64) -> Result<(), Box> { Ok(()) } }