| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| //! The omaha_client::common module contains those types that are common to many parts of the |
| //! library. Many of these don't belong to a specific sub-module. |
| |
| use crate::{ |
| protocol::{self, request::InstallSource, Cohort}, |
| storage::Storage, |
| time::PartialComplexTime, |
| }; |
| use log::error; |
| use serde::{Deserialize, Serialize}; |
| use std::collections::HashMap; |
| use std::fmt; |
| use std::time::Duration; |
| use version::Version; |
| |
| /// Omaha has historically supported multiple methods of counting devices. Currently, the |
| /// only recommended method is the Client Regulated - Date method. |
| /// |
| /// See https://github.com/google/omaha/blob/HEAD/doc/ServerProtocolV3.md#client-regulated-counting-date-based |
| #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] |
| pub enum UserCounting { |
| ClientRegulatedByDate( |
| /// Date (sent by the server) of the last contact with Omaha. |
| Option<i32>, |
| ), |
| } |
| |
| /// Helper implementation to bridge from the protocol to the internal representation for tracking |
| /// the data for client-regulated user counting. |
| impl From<Option<protocol::response::DayStart>> for UserCounting { |
| fn from(opt_day_start: Option<protocol::response::DayStart>) -> Self { |
| match opt_day_start { |
| Some(day_start) => UserCounting::ClientRegulatedByDate(day_start.elapsed_days), |
| None => UserCounting::ClientRegulatedByDate(None), |
| } |
| } |
| } |
| |
| /// The App struct holds information about an application to perform an update check for. |
| #[derive(Clone, Debug, Eq, PartialEq)] |
| pub struct App { |
| /// This is the app_id that Omaha uses to identify a given application. |
| pub id: String, |
| |
| /// This is the current version of the application. |
| pub version: Version, |
| |
| /// This is the fingerprint for the application package. |
| /// |
| /// See https://github.com/google/omaha/blob/HEAD/doc/ServerProtocolV3.md#packages--fingerprints |
| pub fingerprint: Option<String>, |
| |
| /// The app's current cohort information (cohort id, hint, etc). This is both provided to Omaha |
| /// as well as returned by Omaha. |
| pub cohort: Cohort, |
| |
| /// The app's current user-counting information. This is both provided to Omaha as well as |
| /// returned by Omaha. |
| pub user_counting: UserCounting, |
| |
| /// Extra fields to include in requests to Omaha. The client library does not inspect or |
| /// operate on these, it just sends them to the service as part of the "app" objects in each |
| /// request. |
| pub extra_fields: HashMap<String, String>, |
| } |
| |
| /// Structure used to serialize per app data to be persisted. |
| /// Be careful when making changes to this struct to keep backward compatibility. |
| #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] |
| pub struct PersistedApp { |
| pub cohort: Cohort, |
| pub user_counting: UserCounting, |
| } |
| |
| impl From<&App> for PersistedApp { |
| fn from(app: &App) -> Self { |
| PersistedApp { cohort: app.cohort.clone(), user_counting: app.user_counting.clone() } |
| } |
| } |
| |
| impl App { |
| /// Start cnstructing an App. |
| pub fn builder<I: Into<String>, V: Into<Version>>(id: I, version: V) -> AppBuilder { |
| AppBuilder { |
| id: id.into(), |
| version: version.into(), |
| fingerprint: None, |
| cohort: None, |
| user_counting: None, |
| extra_fields: HashMap::new(), |
| } |
| } |
| } |
| |
| /// Builder for an App, enforces minimim requirements are met. |
| pub struct AppBuilder { |
| id: String, |
| version: Version, |
| fingerprint: Option<String>, |
| cohort: Option<Cohort>, |
| user_counting: Option<UserCounting>, |
| extra_fields: HashMap<String, String>, |
| } |
| impl AppBuilder { |
| /// Add a cohort for the app. |
| pub fn with_cohort(mut self, cohort: Cohort) -> Self { |
| self.cohort = Some(cohort); |
| self |
| } |
| |
| /// Add a fingerprint to the constructed app. |
| pub fn with_fingerprint(mut self, fingerprint: impl Into<String>) -> Self { |
| self.fingerprint = Some(fingerprint.into()); |
| self |
| } |
| |
| /// Specify the user-counting mechanism to use |
| pub fn with_user_counting(mut self, user_counting: UserCounting) -> Self { |
| self.user_counting = Some(user_counting); |
| self |
| } |
| |
| /// Add additional fields for including in the update check requests. |
| /// |
| /// # Use with caution |
| /// |
| /// This can overwrite fields used by the protocol itself. |
| pub fn with_extra(mut self, key: impl Into<String>, value: impl Into<String>) -> Self { |
| self.extra_fields.insert(key.into(), value.into()); |
| self |
| } |
| |
| pub fn build(self) -> App { |
| App { |
| id: self.id, |
| version: self.version, |
| fingerprint: self.fingerprint, |
| cohort: self.cohort.unwrap_or_default(), |
| user_counting: self.user_counting.unwrap_or(UserCounting::ClientRegulatedByDate(None)), |
| extra_fields: self.extra_fields, |
| } |
| } |
| } |
| |
| impl App { |
| /// Load data from |storage|, only overwrite existing fields if data exists. |
| pub async fn load<'a>(&'a mut self, storage: &'a impl Storage) { |
| if let Some(app_json) = storage.get_string(&self.id).await { |
| match serde_json::from_str::<PersistedApp>(&app_json) { |
| Ok(persisted_app) => { |
| // Do not overwrite existing fields in app. |
| if self.cohort.id == None { |
| self.cohort.id = persisted_app.cohort.id; |
| } |
| if self.cohort.hint == None { |
| self.cohort.hint = persisted_app.cohort.hint; |
| } |
| if self.cohort.name == None { |
| self.cohort.name = persisted_app.cohort.name; |
| } |
| if self.user_counting == UserCounting::ClientRegulatedByDate(None) { |
| self.user_counting = persisted_app.user_counting; |
| } |
| } |
| Err(e) => { |
| error!("Unable to deserialize PersistedApp from json {}: {}", app_json, e); |
| } |
| } |
| } |
| } |
| |
| /// Persist cohort and user counting to |storage|, will try to set all of them to storage even |
| /// if previous set fails. |
| /// It will NOT call commit() on |storage|, caller is responsible to call commit(). |
| pub async fn persist<'a>(&'a self, storage: &'a mut impl Storage) { |
| let persisted_app = PersistedApp::from(self); |
| match serde_json::to_string(&persisted_app) { |
| Ok(json) => { |
| if let Err(e) = storage.set_string(&self.id, &json).await { |
| error!("Unable to persist cohort id: {}", e); |
| } |
| } |
| Err(e) => { |
| error!("Unable to serialize PersistedApp {:?}: {}", persisted_app, e); |
| } |
| } |
| } |
| |
| /// Get the current channel name from cohort name, returns empty string if no cohort name set |
| /// for the app. |
| pub fn get_current_channel(&self) -> &str { |
| self.cohort.name.as_deref().unwrap_or("") |
| } |
| |
| /// Get the target channel name from cohort hint, fallback to current channel if no hint. |
| pub fn get_target_channel(&self) -> &str { |
| self.cohort.hint.as_deref().unwrap_or_else(|| self.get_current_channel()) |
| } |
| |
| /// Set the cohort hint to |channel|. |
| pub fn set_target_channel(&mut self, channel: Option<String>, id: Option<String>) { |
| self.cohort.hint = channel; |
| if let Some(id) = id { |
| self.id = id; |
| } |
| } |
| |
| pub fn valid(&self) -> bool { |
| !self.id.is_empty() && self.version != Version::from([0]) |
| } |
| } |
| |
| /// Options controlling a single update check |
| #[derive(Clone, Debug, Default, PartialEq, Eq)] |
| pub struct CheckOptions { |
| /// Was this check initiated by a person that's waiting for an answer? |
| /// This is used to ignore the background poll rate, and to be aggressive about |
| /// failing fast, so as not to hang on not receiving a response. |
| pub source: InstallSource, |
| } |
| |
| /// This describes the data around the scheduling of update checks |
| #[derive(Clone, Copy, Default, PartialEq)] |
| pub struct UpdateCheckSchedule { |
| // TODO(fxb/64804): Theoretically last_update_time and last_update_check_time |
| // do not need to coexist and we can do all the reporting we want via |
| // last_update_time. However, the last update check metric doesn't (as currently |
| // worded) match up with what last_update_time actually records. |
| /// When the last update check was attempted (start time of the check process). |
| pub last_update_time: Option<PartialComplexTime>, |
| |
| /// When the last update check was attempted. |
| pub last_update_check_time: Option<PartialComplexTime>, |
| |
| /// When the next update should happen. |
| pub next_update_time: Option<CheckTiming>, |
| } |
| |
| /// The fields used to describe the timing of the next update check. |
| /// |
| /// This exists as a separate type mostly so that it can be moved around atomically, in a little bit |
| /// neater fashion than it could be if it was a tuple of `(PartialComplexTime, Option<Duration>)`. |
| #[derive(Clone, Copy, Debug, PartialEq)] |
| pub struct CheckTiming { |
| /// The upper time bounds on when it should be performed (expressed as along those timelines |
| /// that are valid based on currently known time quality). |
| pub time: PartialComplexTime, |
| |
| /// The minimum wait until the next check, regardless of the wall or monotonic time it should be |
| /// performed at. This is handled separately as it creates a lower bound vs. the upper bound(s) |
| /// that the `time` field provides. |
| pub minimum_wait: Option<Duration>, |
| } |
| |
| impl UpdateCheckSchedule { |
| pub fn builder() -> ScheduleBuilder { |
| ScheduleBuilder::default() |
| } |
| } |
| |
| impl CheckTiming { |
| pub fn builder() -> CheckTimingBuilder { |
| CheckTimingBuilder::default() |
| } |
| } |
| |
| /// This is a builder for the UpdateCheckSchedule. |
| #[derive(Clone, Debug, Default)] |
| pub struct ScheduleBuilder { |
| last_time: Option<PartialComplexTime>, |
| last_check_time: Option<PartialComplexTime>, |
| next_timing: Option<CheckTiming>, |
| } |
| |
| impl ScheduleBuilder { |
| /// Set the last_update_time for the UpdateCheckSchedule that's to be built. |
| /// This method takes both ComplexTime and Option<ComplexTime>. |
| pub fn last_time(mut self, last_update_time: impl Into<Option<PartialComplexTime>>) -> Self { |
| self.last_time = last_update_time.into(); |
| self |
| } |
| |
| /// Set the last_check_time for the under-construction UpdateCheckSchedule. |
| /// This method takes both ComplexTime and Option<ComplexTime>. |
| pub fn last_check_time( |
| mut self, |
| last_check_time: impl Into<Option<PartialComplexTime>>, |
| ) -> Self { |
| self.last_check_time = last_check_time.into(); |
| self |
| } |
| |
| /// Set the CheckTiming for the next update check to use. |
| pub fn next_timing(mut self, next_timing: impl Into<Option<CheckTiming>>) -> Self { |
| self.next_timing = next_timing.into(); |
| self |
| } |
| |
| /// Build the UpdateCheckSchedule. |
| pub fn build(self) -> UpdateCheckSchedule { |
| UpdateCheckSchedule { |
| last_update_time: self.last_time, |
| last_update_check_time: self.last_check_time, |
| next_update_time: self.next_timing, |
| } |
| } |
| } |
| |
| /// This is a builder for the `CheckTiming` struct. |
| /// |
| /// It uses a type-state to ensure that the time of the check has been set before allowing the |
| /// construction of the `CheckTiming` itself. |
| #[derive(Clone, Copy, Debug, Default, PartialEq)] |
| pub struct CheckTimingBuilder; |
| |
| /// This is the internal type-state for ensuring the time has been set. |
| #[derive(Clone, Copy, Debug, PartialEq)] |
| pub struct CheckTimingBuilderWithTime { |
| time: PartialComplexTime, |
| minimum_wait: Option<Duration>, |
| } |
| |
| impl CheckTimingBuilder { |
| /// The time of the next check must be set in order to construct the `CheckTiming` type. |
| pub fn time(self, time: impl Into<PartialComplexTime>) -> CheckTimingBuilderWithTime { |
| CheckTimingBuilderWithTime { time: time.into(), minimum_wait: None } |
| } |
| } |
| |
| impl CheckTimingBuilderWithTime { |
| /// Set the minimum wait until the next check. |
| pub fn minimum_wait(mut self, minimum_wait: impl Into<Option<Duration>>) -> Self { |
| self.minimum_wait = minimum_wait.into(); |
| self |
| } |
| |
| /// Build the CheckTiming. |
| pub fn build(self) -> CheckTiming { |
| CheckTiming { time: self.time, minimum_wait: self.minimum_wait } |
| } |
| } |
| |
| /// Helper struct that provides a nicer format for Debug printing `Option` by dropping the |
| /// `Some(...)` that wraps its value, and instead uses the Display trait implementation of the |
| /// value. |
| /// |
| /// Examples: |
| /// `"MyStruct { option_string_field: None }"` |
| /// `"MyStruct { option_string_field: "string field value" }"` |
| /// |
| pub struct PrettyOptionDisplay<T>(pub Option<T>) |
| where |
| T: fmt::Display; |
| impl<T> fmt::Display for PrettyOptionDisplay<T> |
| where |
| T: fmt::Display, |
| { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| match &self.0 { |
| None => write!(f, "None"), |
| Some(value) => fmt::Display::fmt(value, f), |
| } |
| } |
| } |
| impl<T> fmt::Debug for PrettyOptionDisplay<T> |
| where |
| T: fmt::Display, |
| { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| fmt::Display::fmt(self, f) |
| } |
| } |
| |
| /// The default Debug implementation for SystemTime will only print seconds since unix epoch, which |
| /// is not terribly useful in logs, so this prints a more human-relatable format. |
| /// |
| /// e.g. |
| /// `UpdateCheckSchedule { last_update_time: None, next_uptime_time: None }` |
| /// `UpdateCheckSchedule { last_update_time: "2001-07-08 16:34:56.026 UTC (994518299.026420000)", next_uptime_time: None }` |
| impl fmt::Debug for UpdateCheckSchedule { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| f.debug_struct("UpdateCheckSchedule") |
| .field("last_update_time", &PrettyOptionDisplay(self.last_update_time)) |
| .field("next_update_time", &PrettyOptionDisplay(self.next_update_time)) |
| .finish() |
| } |
| } |
| |
| impl fmt::Display for CheckTiming { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| match self.minimum_wait { |
| None => fmt::Display::fmt(&self.time, f), |
| Some(wait) => write!(f, "{} wait: {:?}", &self.time, &wait), |
| } |
| } |
| } |
| |
| /// These hold the data maintained request-to-request so that the requirements for |
| /// backoffs, throttling, proxy use, etc. can all be properly maintained. This is |
| /// NOT the state machine's internal state. |
| #[derive(Clone, Debug, Default, Eq, PartialEq)] |
| pub struct ProtocolState { |
| /// If the server has dictated the next poll interval, this holds what that |
| /// interval is. |
| pub server_dictated_poll_interval: Option<std::time::Duration>, |
| |
| /// The number of consecutive failed update checks. Used to perform backoffs. |
| pub consecutive_failed_update_checks: u32, |
| |
| /// The number of consecutive proxied requests. Used to periodically not use |
| /// proxies, in the case of an invalid proxy configuration. |
| pub consecutive_proxied_requests: u32, |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::*; |
| use crate::{ |
| storage::MemStorage, |
| time::{MockTimeSource, TimeSource}, |
| }; |
| use futures::executor::block_on; |
| use pretty_assertions::assert_eq; |
| use std::str::FromStr; |
| use std::time::SystemTime; |
| |
| #[test] |
| fn test_app_new_version() { |
| let app = |
| App::builder("some_id", [1, 2]).with_cohort(Cohort::from_hint("some-channel")).build(); |
| assert_eq!(app.id, "some_id"); |
| assert_eq!(app.version, [1, 2].into()); |
| assert_eq!(app.fingerprint, None); |
| assert_eq!(app.cohort.hint, Some("some-channel".to_string())); |
| assert_eq!(app.cohort.name, None); |
| assert_eq!(app.cohort.id, None); |
| assert_eq!(app.user_counting, UserCounting::ClientRegulatedByDate(None)); |
| assert!(app.extra_fields.is_empty(), "Extra fields are not empty"); |
| } |
| |
| #[test] |
| fn test_app_with_fingerprint() { |
| let app = App::builder("some_id_2", [4, 6]) |
| .with_cohort(Cohort::from_hint("test-channel")) |
| .with_fingerprint("some_fp") |
| .build(); |
| assert_eq!(app.id, "some_id_2"); |
| assert_eq!(app.version, [4, 6].into()); |
| assert_eq!(app.fingerprint, Some("some_fp".to_string())); |
| assert_eq!(app.cohort.hint, Some("test-channel".to_string())); |
| assert_eq!(app.cohort.name, None); |
| assert_eq!(app.cohort.id, None); |
| assert_eq!(app.user_counting, UserCounting::ClientRegulatedByDate(None)); |
| assert!(app.extra_fields.is_empty(), "Extra fields are not empty"); |
| } |
| |
| #[test] |
| fn test_app_with_user_counting() { |
| let app = App::builder("some_id_2", [4, 6]) |
| .with_cohort(Cohort::from_hint("test-channel")) |
| .with_user_counting(UserCounting::ClientRegulatedByDate(Some(42))) |
| .build(); |
| assert_eq!(app.id, "some_id_2"); |
| assert_eq!(app.version, [4, 6].into()); |
| assert_eq!(app.cohort.hint, Some("test-channel".to_string())); |
| assert_eq!(app.cohort.name, None); |
| assert_eq!(app.cohort.id, None); |
| assert_eq!(app.user_counting, UserCounting::ClientRegulatedByDate(Some(42))); |
| assert!(app.extra_fields.is_empty(), "Extra fields are not empty"); |
| } |
| |
| #[test] |
| fn test_app_with_extras() { |
| let app = App::builder("some_id_2", [4, 6]) |
| .with_cohort(Cohort::from_hint("test-channel")) |
| .with_extra("key1", "value1") |
| .with_extra("key2", "value2") |
| .build(); |
| assert_eq!(app.id, "some_id_2"); |
| assert_eq!(app.version, [4, 6].into()); |
| assert_eq!(app.cohort.hint, Some("test-channel".to_string())); |
| assert_eq!(app.cohort.name, None); |
| assert_eq!(app.cohort.id, None); |
| assert_eq!(app.user_counting, UserCounting::ClientRegulatedByDate(None)); |
| assert_eq!(app.extra_fields.len(), 2); |
| assert_eq!(app.extra_fields["key1"], "value1"); |
| assert_eq!(app.extra_fields["key2"], "value2"); |
| } |
| |
| #[test] |
| fn test_app_load() { |
| block_on(async { |
| let mut storage = MemStorage::new(); |
| let json = serde_json::json!({ |
| "cohort": { |
| "cohort": "some_id", |
| "cohorthint":"some_hint", |
| "cohortname": "some_name" |
| }, |
| "user_counting": { |
| "ClientRegulatedByDate":123 |
| }}); |
| let json = serde_json::to_string(&json).unwrap(); |
| let mut app = App::builder("some_id", [1, 2]).build(); |
| storage.set_string(&app.id, &json).await.unwrap(); |
| app.load(&storage).await; |
| |
| let cohort = Cohort { |
| id: Some("some_id".to_string()), |
| hint: Some("some_hint".to_string()), |
| name: Some("some_name".to_string()), |
| }; |
| assert_eq!(cohort, app.cohort); |
| assert_eq!(UserCounting::ClientRegulatedByDate(Some(123)), app.user_counting); |
| }); |
| } |
| |
| #[test] |
| fn test_app_load_empty_storage() { |
| block_on(async { |
| let storage = MemStorage::new(); |
| let cohort = Cohort { |
| id: Some("some_id".to_string()), |
| hint: Some("some_hint".to_string()), |
| name: Some("some_name".to_string()), |
| }; |
| let mut app = App::builder("some_id", [1, 2]) |
| .with_cohort(cohort) |
| .with_user_counting(UserCounting::ClientRegulatedByDate(Some(123))) |
| .build(); |
| app.load(&storage).await; |
| |
| // existing data not overwritten |
| let cohort = Cohort { |
| id: Some("some_id".to_string()), |
| hint: Some("some_hint".to_string()), |
| name: Some("some_name".to_string()), |
| }; |
| assert_eq!(cohort, app.cohort); |
| assert_eq!(UserCounting::ClientRegulatedByDate(Some(123)), app.user_counting); |
| }); |
| } |
| |
| #[test] |
| fn test_app_load_malformed() { |
| block_on(async { |
| let mut storage = MemStorage::new(); |
| let cohort = Cohort { |
| id: Some("some_id".to_string()), |
| hint: Some("some_hint".to_string()), |
| name: Some("some_name".to_string()), |
| }; |
| let mut app = App::builder("some_id", [1, 2]) |
| .with_cohort(cohort) |
| .with_user_counting(UserCounting::ClientRegulatedByDate(Some(123))) |
| .build(); |
| storage.set_string(&app.id, "not a json").await.unwrap(); |
| app.load(&storage).await; |
| |
| // existing data not overwritten |
| let cohort = Cohort { |
| id: Some("some_id".to_string()), |
| hint: Some("some_hint".to_string()), |
| name: Some("some_name".to_string()), |
| }; |
| assert_eq!(cohort, app.cohort); |
| assert_eq!(UserCounting::ClientRegulatedByDate(Some(123)), app.user_counting); |
| }); |
| } |
| |
| #[test] |
| fn test_app_load_partial() { |
| block_on(async { |
| let mut storage = MemStorage::new(); |
| let json = serde_json::json!({ |
| "cohort": { |
| "cohorthint":"some_hint_2", |
| "cohortname": "some_name_2" |
| }, |
| "user_counting": { |
| "ClientRegulatedByDate":null |
| }}); |
| let json = serde_json::to_string(&json).unwrap(); |
| let cohort = Cohort { |
| id: Some("some_id".to_string()), |
| hint: Some("some_hint".to_string()), |
| name: Some("some_name".to_string()), |
| }; |
| let mut app = App::builder("some_id", [1, 2]) |
| .with_cohort(cohort) |
| .with_user_counting(UserCounting::ClientRegulatedByDate(Some(123))) |
| .build(); |
| storage.set_string(&app.id, &json).await.unwrap(); |
| app.load(&storage).await; |
| |
| // existing data not overwritten |
| let cohort = Cohort { |
| id: Some("some_id".to_string()), |
| hint: Some("some_hint".to_string()), |
| name: Some("some_name".to_string()), |
| }; |
| assert_eq!(cohort, app.cohort); |
| assert_eq!(UserCounting::ClientRegulatedByDate(Some(123)), app.user_counting); |
| }); |
| } |
| |
| #[test] |
| fn test_app_load_override() { |
| block_on(async { |
| let mut storage = MemStorage::new(); |
| let json = serde_json::json!({ |
| "cohort": { |
| "cohort": "some_id_2", |
| "cohorthint":"some_hint_2", |
| "cohortname": "some_name_2" |
| }, |
| "user_counting": { |
| "ClientRegulatedByDate":123 |
| }}); |
| let json = serde_json::to_string(&json).unwrap(); |
| let cohort = Cohort { |
| id: Some("some_id".to_string()), |
| hint: Some("some_hint".to_string()), |
| name: None, |
| }; |
| let mut app = App::builder("some_id", [1, 2]) |
| .with_cohort(cohort) |
| .with_user_counting(UserCounting::ClientRegulatedByDate(Some(123))) |
| .build(); |
| storage.set_string(&app.id, &json).await.unwrap(); |
| app.load(&storage).await; |
| |
| // existing data not overwritten |
| let cohort = Cohort { |
| id: Some("some_id".to_string()), |
| hint: Some("some_hint".to_string()), |
| name: Some("some_name_2".to_string()), |
| }; |
| assert_eq!(cohort, app.cohort); |
| assert_eq!(UserCounting::ClientRegulatedByDate(Some(123)), app.user_counting); |
| }); |
| } |
| |
| #[test] |
| fn test_app_persist() { |
| block_on(async { |
| let mut storage = MemStorage::new(); |
| let cohort = Cohort { |
| id: Some("some_id".to_string()), |
| hint: Some("some_hint".to_string()), |
| name: Some("some_name".to_string()), |
| }; |
| let app = App::builder("some_id", [1, 2]) |
| .with_cohort(cohort) |
| .with_user_counting(UserCounting::ClientRegulatedByDate(Some(123))) |
| .build(); |
| app.persist(&mut storage).await; |
| |
| let expected = serde_json::json!({ |
| "cohort": { |
| "cohort": "some_id", |
| "cohorthint":"some_hint", |
| "cohortname": "some_name" |
| }, |
| "user_counting": { |
| "ClientRegulatedByDate":123 |
| }}); |
| let json = storage.get_string(&app.id).await.unwrap(); |
| assert_eq!(expected, serde_json::Value::from_str(&json).unwrap()); |
| assert_eq!(false, storage.committed()); |
| }); |
| } |
| |
| #[test] |
| fn test_app_persist_empty() { |
| block_on(async { |
| let mut storage = MemStorage::new(); |
| let cohort = Cohort { id: None, hint: None, name: None }; |
| let app = App::builder("some_id", [1, 2]).with_cohort(cohort).build(); |
| app.persist(&mut storage).await; |
| |
| let expected = serde_json::json!({ |
| "cohort": {}, |
| "user_counting": { |
| "ClientRegulatedByDate":null |
| }}); |
| let json = storage.get_string(&app.id).await.unwrap(); |
| assert_eq!(expected, serde_json::Value::from_str(&json).unwrap()); |
| assert_eq!(false, storage.committed()); |
| }); |
| } |
| |
| #[test] |
| fn test_app_get_current_channel() { |
| let cohort = Cohort { name: Some("current-channel-123".to_string()), ..Cohort::default() }; |
| let app = App::builder("some_id", [0, 1]).with_cohort(cohort).build(); |
| assert_eq!("current-channel-123", app.get_current_channel()); |
| } |
| |
| #[test] |
| fn test_app_get_current_channel_default() { |
| let app = App::builder("some_id", [0, 1]).build(); |
| assert_eq!("", app.get_current_channel()); |
| } |
| |
| #[test] |
| fn test_app_get_target_channel() { |
| let cohort = Cohort::from_hint("target-channel-456"); |
| let app = App::builder("some_id", [0, 1]).with_cohort(cohort).build(); |
| assert_eq!("target-channel-456", app.get_target_channel()); |
| } |
| |
| #[test] |
| fn test_app_get_target_channel_fallback() { |
| let cohort = Cohort { name: Some("current-channel-123".to_string()), ..Cohort::default() }; |
| let app = App::builder("some_id", [0, 1]).with_cohort(cohort).build(); |
| assert_eq!("current-channel-123", app.get_target_channel()); |
| } |
| |
| #[test] |
| fn test_app_get_target_channel_default() { |
| let app = App::builder("some_id", [0, 1]).build(); |
| assert_eq!("", app.get_target_channel()); |
| } |
| |
| #[test] |
| fn test_app_set_target_channel() { |
| let mut app = App::builder("some_id", [0, 1]).build(); |
| assert_eq!("", app.get_target_channel()); |
| app.set_target_channel(Some("new-target-channel".to_string()), None); |
| assert_eq!("new-target-channel", app.get_target_channel()); |
| app.set_target_channel(None, None); |
| assert_eq!("", app.get_target_channel()); |
| } |
| |
| #[test] |
| fn test_app_set_target_channel_and_id() { |
| let mut app = App::builder("some_id", [0, 1]).build(); |
| assert_eq!("", app.get_target_channel()); |
| app.set_target_channel(Some("new-target-channel".to_string()), Some("new-id".to_string())); |
| assert_eq!("new-target-channel", app.get_target_channel()); |
| assert_eq!("new-id", app.id); |
| app.set_target_channel(None, None); |
| assert_eq!("", app.get_target_channel()); |
| assert_eq!("new-id", app.id); |
| } |
| |
| #[test] |
| fn test_app_valid() { |
| let app = App::builder("some_id", [0, 1]).build(); |
| assert!(app.valid()); |
| } |
| |
| #[test] |
| fn test_app_not_valid() { |
| let app = App::builder("", [0, 1]).build(); |
| assert!(!app.valid()); |
| let app = App::builder("some_id", [0]).build(); |
| assert!(!app.valid()); |
| } |
| |
| #[test] |
| fn test_pretty_option_display_with_none() { |
| assert_eq!("None", format!("{:?}", PrettyOptionDisplay(Option::<String>::None))); |
| } |
| |
| #[test] |
| fn test_pretty_option_display_with_some() { |
| assert_eq!("this is a test", format!("{:?}", PrettyOptionDisplay(Some("this is a test")))); |
| } |
| |
| #[test] |
| fn test_update_check_schedule_debug_with_defaults() { |
| assert_eq!( |
| "UpdateCheckSchedule { \ |
| last_update_time: None, \ |
| next_update_time: None \ |
| }", |
| format!("{:?}", UpdateCheckSchedule::default()) |
| ); |
| } |
| |
| #[test] |
| fn test_update_check_schedule_debug_with_values() { |
| let mock_time = MockTimeSource::new_from_now(); |
| let last = mock_time.now(); |
| let next = last + Duration::from_secs(1000); |
| assert_eq!( |
| format!( |
| "UpdateCheckSchedule {{ last_update_time: {}, next_update_time: {} }}", |
| PartialComplexTime::from(last), |
| next |
| ), |
| format!( |
| "{:?}", |
| UpdateCheckSchedule::builder() |
| .last_time(last) |
| .next_timing(CheckTiming::builder().time(next).build()) |
| .build() |
| ) |
| ); |
| } |
| |
| #[test] |
| fn test_update_check_schedule_builder_all_fields() { |
| let mock_time = MockTimeSource::new_from_now(); |
| let now = PartialComplexTime::from(mock_time.now()); |
| assert_eq!( |
| UpdateCheckSchedule::builder() |
| .last_time(PartialComplexTime::from( |
| SystemTime::UNIX_EPOCH + Duration::from_secs(100000) |
| )) |
| .next_timing( |
| CheckTiming::builder().time(now).minimum_wait(Duration::from_secs(100)).build() |
| ) |
| .build(), |
| UpdateCheckSchedule { |
| last_update_time: Some(PartialComplexTime::from( |
| SystemTime::UNIX_EPOCH + Duration::from_secs(100000) |
| )), |
| next_update_time: Some(CheckTiming { |
| time: now, |
| minimum_wait: Some(Duration::from_secs(100)) |
| }), |
| ..Default::default() |
| } |
| ); |
| } |
| |
| #[test] |
| fn test_update_check_schedule_builder_all_fields_from_options() { |
| let next_time = PartialComplexTime::from(MockTimeSource::new_from_now().now()); |
| assert_eq!( |
| UpdateCheckSchedule::builder() |
| .last_time(Some(PartialComplexTime::from( |
| SystemTime::UNIX_EPOCH + Duration::from_secs(100000) |
| ))) |
| .next_timing(Some( |
| CheckTiming::builder() |
| .time(next_time) |
| .minimum_wait(Some(Duration::from_secs(100))) |
| .build() |
| )) |
| .build(), |
| UpdateCheckSchedule { |
| last_update_time: Some(PartialComplexTime::from( |
| SystemTime::UNIX_EPOCH + Duration::from_secs(100000) |
| )), |
| next_update_time: Some(CheckTiming { |
| time: next_time, |
| minimum_wait: Some(Duration::from_secs(100)) |
| }), |
| ..Default::default() |
| } |
| ); |
| } |
| |
| #[test] |
| fn test_update_check_schedule_builder_subset_fields() { |
| assert_eq!( |
| UpdateCheckSchedule::builder() |
| .last_time(PartialComplexTime::from( |
| SystemTime::UNIX_EPOCH + Duration::from_secs(100000) |
| )) |
| .build(), |
| UpdateCheckSchedule { |
| last_update_time: Some(PartialComplexTime::from( |
| SystemTime::UNIX_EPOCH + Duration::from_secs(100000) |
| )), |
| ..Default::default() |
| } |
| ); |
| |
| let next_time = PartialComplexTime::from(MockTimeSource::new_from_now().now()); |
| assert_eq!( |
| UpdateCheckSchedule::builder() |
| .next_timing( |
| CheckTiming::builder() |
| .time(next_time) |
| .minimum_wait(Duration::from_secs(5)) |
| .build() |
| ) |
| .build(), |
| UpdateCheckSchedule { |
| next_update_time: Some(CheckTiming { |
| time: next_time, |
| minimum_wait: Some(Duration::from_secs(5)) |
| }), |
| ..Default::default() |
| } |
| ); |
| } |
| |
| #[test] |
| fn test_update_check_schedule_builder_defaults_are_same_as_default_impl() { |
| assert_eq!( |
| UpdateCheckSchedule::builder().build(), |
| UpdateCheckSchedule { ..Default::default() } |
| ); |
| } |
| } |