use std::{collections::HashMap, sync::Arc, time::Duration};
use crate::cookies::{CookieAccessor, CookieAccessorAdapter};
use kameleoon_core::{
core_client::{CoreKameleoonClient, DataFileUpdateCb},
data::{CustomData, KameleoonData},
error::KameleoonError,
types::{DataFile, RemoteVisitorDataFilter, Variation},
};
use tokio::sync::oneshot;
#[derive(Clone)]
pub struct KameleoonClient {
inner: Arc<CoreKameleoonClient>,
}
impl KameleoonClient {
pub(crate) fn new(inner: Arc<CoreKameleoonClient>) -> Self {
Self { inner }
}
pub async fn initialize(&self) -> Result<(), KameleoonError> {
self.initialize_with_timeout(Duration::ZERO).await
}
pub async fn initialize_with_timeout(&self, timeout: Duration) -> Result<(), KameleoonError> {
let (tx, rx) = oneshot::channel::<Result<(), KameleoonError>>();
self.inner.wait_init(
if timeout != Duration::ZERO { Some(timeout) } else { None },
Box::new(move |res| {
let _ = tx.send(res.clone());
}),
);
rx.await.unwrap_or_else(|_| Ok(()))
}
pub fn is_ready(&self) -> bool {
self.inner.is_ready()
}
pub fn get_visitor_code(
&self,
cookies: &mut impl CookieAccessor,
default_visitor_code: Option<&str>,
) -> Result<String, KameleoonError> {
let mut cookies = CookieAccessorAdapter { inner: Some(cookies) };
self.inner.get_visitor_code(&mut cookies, default_visitor_code)
}
pub fn add_data<D: Into<KameleoonData>>(
&self,
visitor_code: &str,
data: impl IntoIterator<Item = D>,
) -> Result<(), KameleoonError> {
self.add_data_with_track(visitor_code, data, true)
}
pub fn add_data_with_track<D: Into<KameleoonData>>(
&self,
visitor_code: &str,
data: impl IntoIterator<Item = D>,
track: bool,
) -> Result<(), KameleoonError> {
self.inner.add_data(visitor_code, data.into_iter().map(Into::into).collect(), track)
}
pub fn track_conversion(&self, visitor_code: &str, goal_id: u32) -> Result<(), KameleoonError> {
self.track_conversion_with_opts(visitor_code, goal_id, TrackConversionOpts::new())
}
pub fn track_conversion_with_opts(
&self,
visitor_code: &str,
goal_id: u32,
options: TrackConversionOpts,
) -> Result<(), KameleoonError> {
self.inner.track_conversion(visitor_code, goal_id, options.revenue, options.negative, options.metadata)
}
pub fn set_legal_consent(
&self,
visitor_code: &str,
legal_consent: bool,
cookies: Option<&mut impl CookieAccessor>,
) -> Result<(), KameleoonError> {
let mut cookies = CookieAccessorAdapter { inner: cookies };
self.inner.set_legal_consent(visitor_code, legal_consent, Some(&mut cookies))
}
pub fn flush(&self, visitor_code: &str) -> Result<(), KameleoonError> {
self.inner.flush(visitor_code)
}
pub async fn flush_instant(&self, visitor_code: &str) -> Result<(), KameleoonError> {
self.inner.flush_instant_async(visitor_code).await
}
pub fn is_feature_active(&self, visitor_code: &str, feature_key: &str) -> Result<bool, KameleoonError> {
self.is_feature_active_with_opts(visitor_code, feature_key, IsFeatureActiveOpts::new())
}
pub fn is_feature_active_with_opts(
&self,
visitor_code: &str,
feature_key: &str,
options: IsFeatureActiveOpts,
) -> Result<bool, KameleoonError> {
self.inner.is_feature_active(visitor_code, feature_key, options.track)
}
pub fn get_variation(&self, visitor_code: &str, feature_key: &str) -> Result<Variation, KameleoonError> {
self.get_variation_with_opts(visitor_code, feature_key, GetVariationOpts::new())
}
pub fn get_variation_with_opts(
&self,
visitor_code: &str,
feature_key: &str,
options: GetVariationOpts,
) -> Result<Variation, KameleoonError> {
self.inner.get_variation(visitor_code, feature_key, options.track)
}
pub fn get_variations(&self, visitor_code: &str) -> Result<HashMap<String, Variation>, KameleoonError> {
self.get_variations_with_opts(visitor_code, GetVariationsOpts::new())
}
pub fn get_variations_with_opts(
&self,
visitor_code: &str,
options: GetVariationsOpts,
) -> Result<HashMap<String, Variation>, KameleoonError> {
self.inner.get_variations(visitor_code, options.only_active, options.track)
}
pub fn set_forced_variation(
&self,
visitor_code: &str,
experiment_id: u32,
variation_key: Option<&str>,
) -> Result<(), KameleoonError> {
self.set_forced_variation_with_opts(visitor_code, experiment_id, variation_key, SetForcedVariationOpts::new())
}
pub fn set_forced_variation_with_opts(
&self,
visitor_code: &str,
experiment_id: u32,
variation_key: Option<&str>,
options: SetForcedVariationOpts,
) -> Result<(), KameleoonError> {
self.inner.set_forced_variation(visitor_code, experiment_id, variation_key, options.force_targeting)
}
pub fn get_engine_tracking_code(&self, visitor_code: &str) -> Result<String, KameleoonError> {
self.inner.get_engine_tracking_code(visitor_code)
}
pub async fn get_remote_data(&self, key: &str) -> Result<String, KameleoonError> {
self.inner.get_remote_data_async(key).await
}
pub async fn get_remote_visitor_data(
&self,
visitor_code: &str,
filter: Option<RemoteVisitorDataFilter>,
) -> Result<(), KameleoonError> {
self.inner.get_remote_visitor_data_async(visitor_code, filter).await
}
pub async fn get_visitor_warehouse_audience(
&self,
visitor_code: &str,
warehouse_key: Option<&str>,
custom_data_index: u32,
) -> Result<(), KameleoonError> {
self.inner.get_visitor_warehouse_audience_async(visitor_code, warehouse_key, custom_data_index).await
}
pub fn on_datafile_update(&self, handler: Option<DataFileUpdateCb>) {
self.inner.on_datafile_update(handler)
}
pub fn get_feature_keys(&self) -> Result<Vec<String>, KameleoonError> {
Ok(self.inner.get_feature_keys())
}
pub fn get_datafile(&self) -> Result<Arc<DataFile>, KameleoonError> {
Ok(self.inner.get_types_datafile())
}
}
#[derive(Debug, Default)]
pub struct TrackConversionOpts {
revenue: f32,
negative: bool,
metadata: Vec<CustomData>,
}
impl TrackConversionOpts {
pub fn new() -> Self {
Self::default()
}
pub fn revenue(mut self, revenue: f32) -> Self {
self.revenue = revenue;
self
}
pub fn negative(mut self, negative: bool) -> Self {
self.negative = negative;
self
}
pub fn metadata(mut self, metadata: Vec<CustomData>) -> Self {
self.metadata = metadata;
self
}
}
#[derive(Debug)]
pub struct IsFeatureActiveOpts {
track: bool,
}
impl Default for IsFeatureActiveOpts {
fn default() -> Self {
Self { track: true }
}
}
impl IsFeatureActiveOpts {
pub fn new() -> Self {
Self::default()
}
pub fn track(mut self, track: bool) -> Self {
self.track = track;
self
}
}
#[derive(Debug)]
pub struct GetVariationOpts {
track: bool,
}
impl Default for GetVariationOpts {
fn default() -> Self {
Self { track: true }
}
}
impl GetVariationOpts {
pub fn new() -> Self {
Self::default()
}
pub fn track(mut self, track: bool) -> Self {
self.track = track;
self
}
}
#[derive(Debug)]
pub struct GetVariationsOpts {
only_active: bool,
track: bool,
}
impl Default for GetVariationsOpts {
fn default() -> Self {
Self {
only_active: false,
track: true,
}
}
}
impl GetVariationsOpts {
pub fn new() -> Self {
Self::default()
}
pub fn only_active(mut self, only_active: bool) -> Self {
self.only_active = only_active;
self
}
pub fn track(mut self, track: bool) -> Self {
self.track = track;
self
}
}
#[derive(Debug)]
pub struct SetForcedVariationOpts {
force_targeting: bool,
}
impl Default for SetForcedVariationOpts {
fn default() -> Self {
Self { force_targeting: true }
}
}
impl SetForcedVariationOpts {
pub fn new() -> Self {
Self::default()
}
pub fn force_targeting(mut self, force_targeting: bool) -> Self {
self.force_targeting = force_targeting;
self
}
}
#[cfg(test)]
mod tests {
use std::{
collections::HashMap,
sync::{
Arc, OnceLock,
atomic::{AtomicBool, Ordering},
},
time::{Duration, SystemTime, UNIX_EPOCH},
vec,
};
use crate::{
client::{
GetVariationOpts, GetVariationsOpts, IsFeatureActiveOpts, SetForcedVariationOpts, TrackConversionOpts,
},
config::KameleoonClientConfig,
cookies::CookieAccessor,
data::{
ApplicationVersion, Browser, BrowserKind, Conversion, ConversionOpts, Cookie, CustomData, Device,
DeviceKind, GeolocationBuilder, KameleoonData, OperatingSystem, OperatingSystemKind, PageView,
UniqueIdentifier, UserAgent,
},
error::ErrorCode,
};
use kameleoon_core::core_client::CoreKameleoonClient;
use reqwest::Client;
use tokio::{runtime::Runtime, sync::OnceCell};
use super::KameleoonClient;
const SITE_CODE: &str = "tndueuutdq";
const WRONG_SITE_CODE: &str = "wrong_site_code";
const SDK_NAME: &str = "RUST";
const SDK_VERSION: &str = env!("CARGO_PKG_VERSION");
const CONFIG_FILE_PATH: &str = "/../../core/core/resources/config.json";
const FEATURE_ALWAYS_ON: &str = "complextargeting_2";
const REMOTE_VISITOR_DATA_URL: &str =
"https://eu-data.kameleoon.io/visit/visitor?currentVisit=true&maxNumberPreviousVisits=25&all=true";
const REMOTE_VISITOR_DATA_SNIPPET_VARIATION: &str = "{\"id\":139649,\"variationId\":1275388}";
const REMOTE_VISITOR_DATA_RETRIES: usize = 10;
const REMOTE_VISITOR_DATA_RETRY_DELAY: Duration = Duration::from_millis(300);
fn read_config() -> KameleoonClientConfig {
let config_path = format!("{}{CONFIG_FILE_PATH}", env!("CARGO_MANIFEST_DIR"));
KameleoonClientConfig::read_from_file(&config_path).unwrap()
}
fn get_client(site_code: &str) -> KameleoonClient {
let inner = Arc::new(
CoreKameleoonClient::new(site_code.to_owned(), read_config(), SDK_NAME.to_owned(), SDK_VERSION.to_owned())
.unwrap(),
);
KameleoonClient::new(inner)
}
static TEST_RUNTIME: OnceLock<Runtime> = OnceLock::new();
static CLIENT: OnceCell<KameleoonClient> = OnceCell::const_new();
fn test_runtime() -> &'static Runtime {
TEST_RUNTIME.get_or_init(|| Runtime::new().expect("Failed to create test runtime"))
}
async fn shared_client() -> &'static KameleoonClient {
CLIENT
.get_or_init(|| async {
let client = get_client(SITE_CODE);
client.initialize().await.unwrap();
client
})
.await
}
fn generate_test_visitor_code() -> String {
let timestamp = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_nanos();
format!("visitor_{timestamp}")
}
async fn assert_remote_visitor_data_contains_expected_text(
site_code: &str,
visitor_code: &str,
expected_texts: &[&str],
) {
let url = format!("{REMOTE_VISITOR_DATA_URL}&siteCode={site_code}&visitorCode={visitor_code}");
let http_client = Client::new();
let mut last_response = String::new();
for attempt in 0..REMOTE_VISITOR_DATA_RETRIES {
let response = http_client.get(&url).send().await;
let respone_text = response.unwrap().error_for_status().unwrap().text().await.unwrap();
let missing_texts: Vec<_> =
expected_texts.iter().copied().filter(|expected_text| !respone_text.contains(expected_text)).collect();
if missing_texts.is_empty() {
return;
}
last_response = respone_text;
if attempt + 1 < REMOTE_VISITOR_DATA_RETRIES {
tokio::time::sleep(REMOTE_VISITOR_DATA_RETRY_DELAY).await;
}
}
panic!(
r#"Remote visitor data did not contain expected snippet(s) {:?} for visitor `{visitor_code}`.
Last response: {last_response}."#,
expected_texts
);
}
struct CookieTest {
cookies: HashMap<String, String>,
}
impl CookieAccessor for CookieTest {
fn set(&mut self, key: &str, value: &str, _max_age: u32, _top_level_domain: Option<&str>) {
self.cookies.insert(key.to_string(), value.to_string());
}
fn get(&self, key: &str) -> Option<&str> {
self.cookies.get(key).map(String::as_str)
}
}
#[tokio::test]
async fn test_initialize_with_timeout_ok() {
let client = get_client(SITE_CODE);
client.initialize_with_timeout(Duration::from_secs(5)).await.unwrap();
assert!(client.is_ready());
}
#[tokio::test]
async fn test_initialize_with_timeout_err_timeout() {
let client = get_client(SITE_CODE);
let timeout_err = client.initialize_with_timeout(Duration::from_millis(1)).await.unwrap_err();
assert_eq!(timeout_err.code(), ErrorCode::Initialization);
assert_eq!(timeout_err.message(), "KameleoonClient initialization timed out after 1ms");
assert!(!client.is_ready());
}
#[tokio::test]
async fn test_initialize_with_timeout_err_wrong_site_code() {
let client = get_client(WRONG_SITE_CODE);
assert!(client.initialize_with_timeout(Duration::from_secs(2)).await.is_err());
assert!(!client.is_ready());
}
#[tokio::test]
async fn test_initialize() {
let client = get_client(SITE_CODE);
client.initialize().await.unwrap();
}
#[tokio::test]
async fn test_is_ready() {
let client = get_client(SITE_CODE);
client.initialize().await.unwrap();
assert!(client.is_ready());
}
#[test]
fn test_get_visitor_code() {
test_runtime().block_on(async {
const COOKIE_KEY: &str = "kameleoonVisitorCode";
let client: &KameleoonClient = shared_client().await;
let mut cookies = CookieTest {
cookies: HashMap::new(),
};
let visitor_code = client.get_visitor_code(&mut cookies, None).unwrap();
assert!(visitor_code.len() == 16);
assert!(cookies.get(COOKIE_KEY).map_or(false, |v| v == visitor_code));
});
}
#[test]
fn test_add_data_and_flush() {
test_runtime().block_on(async {
const REMOTE_VISITOR_DATA_SNIPPET_CONVERSION: &str =
"{\"goalId\":238420,\"revenue\":10.0,\"negative\":true";
const REMOTE_VISITOR_DATA_SNIPPET_CUSTOM_DATA: &str =
"{\"index\":4,\"valuesCountMap\":{\"true\":1},\"overwrite\":true,\"mappingIdentifier\":false}";
const REMOTE_VISITOR_DATA_SNIPPET_GEOLOCATION: &str = "{\"country\":\"co\"}";
const REMOTE_VISITOR_DATA_SNIPPET_PAGE_VIEW: &str = "{\"href\":\"url\"";
const GOAL_ID: u32 = 238420;
let visitor_code = generate_test_visitor_code();
let client: &KameleoonClient = shared_client().await;
let data: Vec<KameleoonData> = vec![
Browser::new(BrowserKind::Safari, 10.0).into(),
Device::new(DeviceKind::Phone).into(),
Cookie::new(HashMap::new()).into(),
Conversion::new_with_opts(
GOAL_ID,
ConversionOpts::new().revenue(10.0).negative(true).metadata(vec![
CustomData::new_with_index(3, vec!["metadata1".to_owned(), "md2".to_owned()]),
CustomData::new_with_index(5, vec!["md3".to_owned()]),
]),
)
.into(),
CustomData::new_with_index(4, vec!["true".to_owned()]).into(),
GeolocationBuilder::default().country("co").build().unwrap().into(),
OperatingSystem::new(OperatingSystemKind::Android).into(),
PageView::new_with_url("url").into(),
UniqueIdentifier::new(false).into(),
UserAgent::new("ua").into(),
ApplicationVersion::new("10.0.0").into(),
];
client.add_data(&visitor_code, data).unwrap();
client.flush_instant(&visitor_code).await.unwrap();
assert_remote_visitor_data_contains_expected_text(
SITE_CODE,
&visitor_code,
&[
REMOTE_VISITOR_DATA_SNIPPET_CONVERSION,
REMOTE_VISITOR_DATA_SNIPPET_CUSTOM_DATA,
REMOTE_VISITOR_DATA_SNIPPET_GEOLOCATION,
REMOTE_VISITOR_DATA_SNIPPET_PAGE_VIEW,
],
)
.await;
});
}
#[test]
fn test_is_feature_active() {
test_runtime().block_on(async {
let visitor_code = generate_test_visitor_code();
let client = shared_client().await;
let active = client.is_feature_active(&visitor_code, FEATURE_ALWAYS_ON).unwrap();
assert!(active);
client.flush_instant(&visitor_code).await.unwrap();
assert_remote_visitor_data_contains_expected_text(
SITE_CODE,
&visitor_code,
&[REMOTE_VISITOR_DATA_SNIPPET_VARIATION],
)
.await;
});
}
#[test]
fn test_is_feature_active_options_default() {
let options = IsFeatureActiveOpts::new();
assert!(options.track);
}
#[test]
fn test_get_variation() {
test_runtime().block_on(async {
let visitor_code = generate_test_visitor_code();
let client = shared_client().await;
let variation = client.get_variation(&visitor_code, FEATURE_ALWAYS_ON).unwrap();
assert_eq!("on", variation.key);
assert_eq!("On", variation.name);
assert_eq!(Some(1275388), variation.id);
assert_eq!(Some(139649), variation.experiment_id);
let variable_value = variation.get_variable("variable").and_then(|v| v.value.as_number()).unwrap_or(0.0);
assert_eq!(10.0, variable_value);
client.flush_instant(&visitor_code).await.unwrap();
assert_remote_visitor_data_contains_expected_text(
SITE_CODE,
&visitor_code,
&[REMOTE_VISITOR_DATA_SNIPPET_VARIATION],
)
.await;
});
}
#[test]
fn test_get_variation_options_default() {
let options = GetVariationOpts::new();
assert!(options.track);
}
#[test]
fn test_get_variations() {
test_runtime().block_on(async {
let visitor_code = generate_test_visitor_code();
let client = shared_client().await;
let variations = client.get_variations(&visitor_code).unwrap();
assert!(variations.contains_key(FEATURE_ALWAYS_ON));
client.flush_instant(&visitor_code).await.unwrap();
assert_remote_visitor_data_contains_expected_text(
SITE_CODE,
&visitor_code,
&[REMOTE_VISITOR_DATA_SNIPPET_VARIATION],
)
.await;
});
}
#[test]
fn test_get_variations_options_default() {
let options = GetVariationsOpts::new();
assert!(!options.only_active);
assert!(options.track);
}
#[test]
fn test_set_forced_variation() {
test_runtime().block_on(async {
const FF_KEY: &str = "ff_new_rules";
const FORCED_VAR: &str = "variation_2";
let visitor_code = "visitor_code_set_forced_variation";
let client = shared_client().await;
let varition = client.get_variation(&visitor_code, FF_KEY).unwrap();
assert_eq!(varition.key, "on");
client.set_forced_variation(visitor_code, 202387, Some(FORCED_VAR)).unwrap();
let varition = client.get_variation(&visitor_code, FF_KEY).unwrap();
assert_eq!(varition.key, FORCED_VAR);
});
}
#[test]
fn test_set_forced_variation_options_default() {
let options = SetForcedVariationOpts::new();
assert!(options.force_targeting);
}
#[test]
fn test_track_conversion() {
test_runtime().block_on(async {
const REMOTE_VISITOR_DATA_SNIPPET_CONVERSION: &str =
"{\"goalId\":238420,\"revenue\":10.0,\"negative\":true";
const GOAL_ID: u32 = 238420;
let visitor_code = generate_test_visitor_code();
let client = shared_client().await;
let _ = client.track_conversion_with_opts(
&visitor_code,
GOAL_ID,
TrackConversionOpts::default().revenue(10.0).negative(true),
);
client.flush_instant(&visitor_code).await.unwrap();
assert_remote_visitor_data_contains_expected_text(
SITE_CODE,
&visitor_code,
&[REMOTE_VISITOR_DATA_SNIPPET_CONVERSION],
)
.await;
});
}
#[test]
fn test_track_conversion_options_default() {
let options = TrackConversionOpts::new();
assert_eq!(options.revenue, 0.0);
assert_eq!(options.negative, false);
assert!(options.metadata.is_empty());
}
#[test]
fn test_set_legal_consent() {
test_runtime().block_on(async {
const SITE_CODE: &str = "u7sxr57uqf";
const REMOTE_VISITOR_DATA_SNIPPET_VARIATION: &str = "{\"id\":376951,\"variationId\":1276549}";
let visitor_code = generate_test_visitor_code();
let client = get_client(SITE_CODE);
client.initialize().await.unwrap();
client.set_legal_consent(&visitor_code, true, None::<&mut CookieTest>).unwrap();
let variation = client.get_variation(&visitor_code, "consent_required_flag").unwrap();
assert_eq!("on", variation.key);
client.flush_instant(&visitor_code).await.unwrap();
assert_remote_visitor_data_contains_expected_text(
SITE_CODE,
&visitor_code,
&[REMOTE_VISITOR_DATA_SNIPPET_VARIATION],
)
.await;
});
}
#[test]
fn test_get_remote_data() {
test_runtime().block_on(async {
let client = shared_client().await;
let remote_data = client.get_remote_data("test 3").await.unwrap();
assert_eq!(
"{\"hobbies\":[\"football\",\"movies\",\"3\"],\"likesKameleoon\":true,\"age\":26.0,\"test-4\":\"no value\",\"test-3\":\"no value\"}",
remote_data
);
});
}
#[test]
fn test_get_remote_visitor_data() {
test_runtime().block_on(async {
const VISITOR_CODE: &str = "visitor_get_remote_visitor_data";
let client = shared_client().await;
let remote_visitor_data = client.get_remote_visitor_data(VISITOR_CODE, None).await;
assert!(remote_visitor_data.is_ok());
});
}
#[test]
fn test_get_visitor_warehouse_audience() {
test_runtime().block_on(async {
const VISITOR_CODE: &str = "visitor_audiences";
let client = shared_client().await;
let remote_visitor_data = client.get_visitor_warehouse_audience(VISITOR_CODE, None, 98).await;
assert!(remote_visitor_data.is_ok());
});
}
#[test]
fn test_get_engine_tracking_code() {
test_runtime().block_on(async {
const VISITOR_CODE: &str = "visitor_engine_tracking_code";
let client = shared_client().await;
let _ = client.is_feature_active(VISITOR_CODE, "ff_new_rules");
let tracking_code = client.get_engine_tracking_code(VISITOR_CODE);
let script_code = concat!(
"window.kameleoonQueue=window.kameleoonQueue||[];",
"window.kameleoonQueue.push(['Experiments.assignVariation',202387,859381,true]);",
"window.kameleoonQueue.push(['Experiments.trigger',202387,true]);"
);
assert_eq!(tracking_code.unwrap(), script_code)
});
}
#[test]
fn test_on_datafile_update_called() {
test_runtime().block_on(async {
let client = get_client(SITE_CODE);
let was_called = Arc::new(AtomicBool::new(false));
let was_called_for_handler = was_called.clone();
client.on_datafile_update(Some(Box::new(move || {
was_called_for_handler.store(true, Ordering::Relaxed);
})));
client.initialize().await.unwrap();
assert!(
was_called.load(Ordering::Relaxed),
"on_datafile_update should be called before initialize returns"
);
});
}
#[test]
fn test_get_feature_keys() {
test_runtime().block_on(async {
#[rustfmt::skip]
let mut expected_values = vec![
"bucketing_custom_data", "complextargeting", "complextargeting_2", "copy_copy_copy_test_first_level_or_operator",
"copy_copy_test_first_level_or_operator", "copy_test_first_level_or_operator", "feature_flag_campaign_conditions",
"feature_flag_can_change_segment__no_tests_for_this_ff_", "feature_flag_disabled_all_enviroments", "feature_flag_liga_stavok",
"feature_flag_multi_environment_test", "feature_flag_test_real_time_configuration_service", "feature_flag_test_scheduling",
"ff_new_rules", "sdk_android_technical_conditions", "sdk_campaigns", "sdk_ios_technical_conditions", "sdk_js",
"sdk_segments", "sdk_technical_conditions", "sdk_utility", "sdk_visit_behavior", "sdk_visited_pages",
"test_feature_variables", "test_first_level_or_operator", "test_pagination_1", "test_pagination_10", "test_pagination_11",
"test_pagination_12", "test_pagination_2", "test_pagination_3_", "test_pagination_4", "test_pagination_5",
"test_pagination_6", "test_pagination_7", "test_pagination_8", "test_pagination_9", "test_segment_lower_greater",
"test_visitorcode___goal_conversion", "testff", "mutually_exclusive_flag_1", "mutually_exclusive_flag_2",
];
expected_values.sort_unstable();
let mut feature_flags = shared_client().await.get_feature_keys().unwrap();
feature_flags.sort_unstable();
assert_eq!(expected_values, feature_flags);
});
}
#[test]
fn test_get_datafile() {
test_runtime().block_on(async {
let client = shared_client().await;
let datafile = client.get_datafile().unwrap();
let variable_flag = datafile.feature_flags.get("test_feature_variables").unwrap();
let on_variation = variable_flag.variations.get("on").unwrap();
assert_eq!(10, on_variation.variables.len());
});
}
}