1use crate::namespace::Namespace;
57use tokio::sync::RwLock;
58use cache::Cache;
59use client_config::ClientConfig;
60use log::{error, trace};
61use std::{collections::HashMap, sync::Arc};
62use wasm_bindgen::prelude::wasm_bindgen;
63
64#[cfg(all(feature = "native-tls", feature = "rustls", not(target_arch = "wasm32")))]
65compile_error!(
66 "Features 'native-tls' and 'rustls' are mutually exclusive on non-WASM targets. \
67 Please disable default features and enable only one."
68);
69
70#[cfg(all(feature = "rustls", target_arch = "wasm32"))]
71compile_error!("Feature 'rustls' is not supported on WASM targets. Only native-tls (browser) is supported.");
72
73cfg_if::cfg_if! {
74 if #[cfg(not(target_arch = "wasm32"))] {
75 use tokio::spawn as spawn;
76 }
77}
78
79mod cache;
80
81pub mod client_config;
82pub mod namespace;
83
84#[derive(Debug, thiserror::Error)]
134pub enum Error {
135 #[error("Client is already running")]
141 AlreadyRunning,
142
143 #[error("Namespace error: {0}")]
148 Namespace(#[from] namespace::Error),
149
150 #[error("Cache error: {0}")]
155 Cache(#[from] cache::Error),
156}
157
158impl From<Error> for wasm_bindgen::JsValue {
159 fn from(error: Error) -> Self {
160 cfg_if::cfg_if! {
161 if #[cfg(target_arch = "wasm32")] {
162 js_sys::Error::new(&error.to_string()).into()
163 } else {
164 error.to_string().into()
165 }
166 }
167 }
168}
169
170cfg_if::cfg_if! {
176 if #[cfg(target_arch = "wasm32")] {
177 pub type EventListener = Arc<dyn Fn(Result<Namespace, Error>)>;
181 } else {
182 pub type EventListener = Arc<dyn Fn(Result<Namespace, Error>) + Send + Sync>;
190 }
191}
192
193#[wasm_bindgen]
241pub struct Client {
242 config: ClientConfig,
247
248 namespaces: Arc<RwLock<HashMap<String, Arc<Cache>>>>,
254
255 handle: Option<tokio::task::JoinHandle<()>>,
261
262 running: Arc<RwLock<bool>>,
267
268 http_client: reqwest::Client,
272}
273
274impl Client {
275 pub(crate) async fn cache(&self, namespace: &str) -> Arc<Cache> {
285 let mut namespaces = self.namespaces.write().await;
286 let cache = namespaces.entry(namespace.to_string()).or_insert_with(|| {
287 trace!("Cache miss, creating cache for namespace {namespace}");
288 Arc::new(Cache::new(
289 self.config.clone(),
290 namespace,
291 self.http_client.clone(),
292 ))
293 });
294 cache.clone()
295 }
296
297 pub async fn add_listener(&self, namespace: &str, listener: EventListener) {
298 let mut namespaces = self.namespaces.write().await;
299 let cache = namespaces.entry(namespace.to_string()).or_insert_with(|| {
300 trace!("Cache miss, creating cache for namespace {namespace}");
301 Arc::new(Cache::new(
302 self.config.clone(),
303 namespace,
304 self.http_client.clone(),
305 ))
306 });
307 cache.add_listener(listener).await;
308 }
309
310 pub async fn namespace(&self, namespace: &str) -> Result<namespace::Namespace, Error> {
375 let cache = self.cache(namespace).await;
376 let value = cache.get_value().await?;
377 Ok(namespace::get_namespace(namespace, value)?)
378 }
379
380 pub async fn start(&mut self) -> Result<(), Error> {
403 let mut running = self.running.write().await;
404 if *running {
405 return Err(Error::AlreadyRunning);
406 }
407
408 *running = true;
409
410 cfg_if::cfg_if! {
411 if #[cfg(target_arch = "wasm32")] {
412 self.handle = None;
413 } else {
414 let running = self.running.clone();
415 let namespaces = self.namespaces.clone();
416 let refresh_interval = {
417 let v = self.config.refresh_interval.unwrap_or(30);
418 let min_val = if cfg!(test) { 1 } else { 30 };
419 if v < min_val { min_val } else { v }
420 };
421 let handle = spawn(async move {
423 loop {
424 let running = running.read().await;
425 if !*running {
426 break;
427 }
428
429 let cache_refs: Vec<_> = {
431 let namespaces = namespaces.read().await;
432 namespaces.iter().map(|(k, v)| (k.clone(), v.clone())).collect()
433 }; for (namespace, cache) in cache_refs {
437 if let Err(err) = cache.refresh().await {
438 error!("Failed to refresh cache for namespace {namespace}: {err:?}");
439 } else {
440 log::debug!("Successfully refreshed cache for namespace {namespace}");
441 }
442 }
443
444 tokio::time::sleep(std::time::Duration::from_secs(refresh_interval)).await;
446 }
447 });
448 self.handle = Some(handle);
449 }
450 }
451
452 Ok(())
453 }
454
455 pub async fn stop(&mut self) {
465 let mut running = self.running.write().await;
466 *running = false;
467
468 cfg_if::cfg_if! {
469 if #[cfg(not(target_arch = "wasm32"))] {
470 if let Some(handle) = self.handle.take() {
471 handle.abort();
472 }
473 }
474 }
475 }
476
477 pub async fn preload(&self, namespaces: &[impl AsRef<str>]) -> Result<(), Error> {
529 #[cfg(not(target_arch = "wasm32"))]
530 let mut tasks = Vec::new();
531
532 #[cfg(target_arch = "wasm32")]
533 {
534 for namespace in namespaces {
535 let cache = self.cache(namespace.as_ref()).await;
536 cache.get_value().await?;
537 }
538 }
539
540 #[cfg(not(target_arch = "wasm32"))]
541 {
542 for namespace in namespaces {
543 let cache = self.cache(namespace.as_ref()).await;
544 let task = tokio::spawn(async move { cache.get_value().await });
545 tasks.push(task);
546 }
547
548 for task in tasks {
550 let result = task.await.map_err(|e| {
551 Error::Cache(cache::Error::Io(std::io::Error::other(format!(
552 "Preload task failed: {e}"
553 ))))
554 })?;
555 result?;
556 }
557 }
558
559 Ok(())
560 }
561}
562
563#[wasm_bindgen]
564impl Client {
565 #[wasm_bindgen(constructor)]
575 #[must_use]
576 pub fn new(config: ClientConfig) -> Self {
577 let http_client = {
578 cfg_if::cfg_if! {
579 if #[cfg(not(target_arch = "wasm32"))] {
580 if let Some(custom_client) = config.http_client.clone() {
581 custom_client
582 } else if config.allow_insecure_https.unwrap_or(false) {
583 reqwest::Client::builder()
584 .danger_accept_invalid_certs(true)
585 .danger_accept_invalid_hostnames(true)
586 .build()
587 .unwrap_or_else(|_| reqwest::Client::new())
588 } else {
589 reqwest::Client::new()
590 }
591 } else {
592 if config.allow_insecure_https.unwrap_or(false) {
593 log::warn!(
594 "allow_insecure_https is silently ignored on wasm32 targets \
595 because SSL/TLS cert validation is strictly controlled by the browser sandbox environment."
596 );
597 }
598 reqwest::Client::new()
599 }
600 }
601 };
602
603 Self {
604 config,
605 namespaces: Arc::new(RwLock::new(HashMap::new())),
606 handle: None,
607 running: Arc::new(RwLock::new(false)),
608 http_client,
609 }
610 }
611
612 #[cfg(target_arch = "wasm32")]
642 #[wasm_bindgen(js_name = "add_listener")]
643 pub async fn add_listener_wasm(&self, namespace: &str, js_listener: js_sys::Function) {
644 let js_listener_clone = js_listener.clone();
645
646 let event_listener: EventListener = Arc::new(move |result: Result<Namespace, Error>| {
647 let err_js_val: wasm_bindgen::JsValue;
648 let data_js_val: wasm_bindgen::JsValue;
649
650 match result {
651 Ok(value) => {
652 data_js_val = value.into();
653 err_js_val = wasm_bindgen::JsValue::UNDEFINED;
654 }
655 Err(cache_error) => {
656 err_js_val = cache_error.into();
657 data_js_val = wasm_bindgen::JsValue::UNDEFINED;
658 }
659 };
660
661 match js_listener_clone.call2(
663 &wasm_bindgen::JsValue::UNDEFINED,
664 &data_js_val,
665 &err_js_val,
666 ) {
667 Ok(_) => {
668 }
670 Err(e) => {
671 log::error!("JavaScript listener threw an error: {:?}", e);
673 }
674 }
675 });
676
677 self.add_listener(namespace, event_listener).await; }
679
680 #[cfg(target_arch = "wasm32")]
681 #[wasm_bindgen(js_name = "namespace")]
682 pub async fn namespace_wasm(&self, namespace: &str) -> Result<wasm_bindgen::JsValue, Error> {
683 let cache = self.cache(namespace).await;
684 let value = cache.get_value().await?;
685 Ok(namespace::get_namespace(namespace, value)?.into())
686 }
687}
688
689#[cfg(test)]
690pub(crate) struct TempDir {
691 path: std::path::PathBuf,
692}
693
694#[cfg(test)]
695impl TempDir {
696 pub(crate) fn new(name: &str) -> Self {
697 let path = std::env::temp_dir().join(name);
698 let _ = std::fs::create_dir_all(&path);
700 Self { path }
701 }
702
703 pub(crate) fn path(&self) -> &std::path::Path {
704 &self.path
705 }
706}
707
708#[cfg(test)]
709impl Drop for TempDir {
710 fn drop(&mut self) {
711 let _ = std::fs::remove_dir_all(&self.path);
713 }
714}
715
716#[cfg(test)]
717pub(crate) fn setup() {
718 cfg_if::cfg_if! {
719 if #[cfg(target_arch = "wasm32")] {
720 let _ = wasm_logger::init(wasm_logger::Config::default());
721 console_error_panic_hook::set_once();
722 } else {
723 let _ = env_logger::builder().is_test(true).try_init();
724 }
725 }
726}
727
728#[cfg(test)]
729mod tests {
730 use super::*;
731
732 use std::sync::Mutex;
733
734 fn test_server_url() -> String {
735 std::env::var("APOLLO_TEST_SERVER").unwrap_or_else(|_| String::from("http://localhost:8080"))
736 }
737
738 fn test_cache_dir() -> String {
739 std::env::temp_dir().join("apollo").to_string_lossy().to_string()
740 }
741
742 #[cfg(not(target_arch = "wasm32"))]
743 pub(crate) static CLIENT_NO_SECRET: std::sync::LazyLock<Client> =
744 std::sync::LazyLock::new(|| {
745 let config = ClientConfig {
746 app_id: String::from("101010101"),
747 cluster: String::from("default"),
748 config_server: test_server_url(),
749 label: None,
750 secret: None,
751 cache_dir: Some(test_cache_dir()),
752 ip: None,
753 allow_insecure_https: None,
754 #[cfg(not(target_arch = "wasm32"))]
755 cache_ttl: None,
756 #[cfg(not(target_arch = "wasm32"))]
757 refresh_interval: None,
758 #[cfg(not(target_arch = "wasm32"))]
759 http_client: None,
760 };
761 Client::new(config)
762 });
763
764 #[cfg(not(target_arch = "wasm32"))]
765 pub(crate) static CLIENT_WITH_SECRET: std::sync::LazyLock<Client> =
766 std::sync::LazyLock::new(|| {
767 let config = ClientConfig {
768 app_id: String::from("101010102"),
769 cluster: String::from("default"),
770 config_server: test_server_url(),
771 label: None,
772 secret: Some(String::from("53bf47631db540ac9700f0020d2192c8")),
773 cache_dir: Some(test_cache_dir()),
774 ip: None,
775 allow_insecure_https: None,
776 #[cfg(not(target_arch = "wasm32"))]
777 cache_ttl: None,
778 #[cfg(not(target_arch = "wasm32"))]
779 refresh_interval: None,
780 #[cfg(not(target_arch = "wasm32"))]
781 http_client: None,
782 };
783 Client::new(config)
784 });
785
786 #[cfg(not(target_arch = "wasm32"))]
787 pub(crate) static CLIENT_WITH_GRAYSCALE_IP: std::sync::LazyLock<Client> =
788 std::sync::LazyLock::new(|| {
789 let config = ClientConfig {
790 app_id: String::from("101010101"),
791 cluster: String::from("default"),
792 config_server: test_server_url(),
793 label: None,
794 secret: None,
795 cache_dir: Some(test_cache_dir()),
796 ip: Some(String::from("1.2.3.4")),
797 allow_insecure_https: None,
798 #[cfg(not(target_arch = "wasm32"))]
799 cache_ttl: None,
800 #[cfg(not(target_arch = "wasm32"))]
801 refresh_interval: None,
802 #[cfg(not(target_arch = "wasm32"))]
803 http_client: None,
804 };
805 Client::new(config)
806 });
807
808 #[cfg(not(target_arch = "wasm32"))]
809 pub(crate) static CLIENT_WITH_GRAYSCALE_LABEL: std::sync::LazyLock<Client> =
810 std::sync::LazyLock::new(|| {
811 let config = ClientConfig {
812 app_id: String::from("101010101"),
813 cluster: String::from("default"),
814 config_server: test_server_url(),
815 label: Some(String::from("GrayScale")),
816 secret: None,
817 cache_dir: Some(test_cache_dir()),
818 ip: None,
819 allow_insecure_https: None,
820 #[cfg(not(target_arch = "wasm32"))]
821 cache_ttl: None,
822 #[cfg(not(target_arch = "wasm32"))]
823 refresh_interval: None,
824 #[cfg(not(target_arch = "wasm32"))]
825 http_client: None,
826 };
827 Client::new(config)
828 });
829
830 #[cfg(not(target_arch = "wasm32"))]
831 #[tokio::test]
832 async fn test_missing_value() {
833 setup();
834 let namespace::Namespace::Properties(properties) =
835 CLIENT_NO_SECRET.namespace("application").await.unwrap()
836 else {
837 panic!("Expected Properties namespace");
838 };
839
840 assert_eq!(properties.get_property::<String>("missingValue"), None);
841 }
842
843 #[cfg(target_arch = "wasm32")]
844 #[wasm_bindgen_test::wasm_bindgen_test]
845 #[allow(dead_code)]
846 async fn test_missing_value_wasm() {
847 setup();
848 let client = create_client_no_secret();
849 let namespace = client.namespace("application").await;
850 match namespace {
851 Ok(namespace) => match namespace {
852 namespace::Namespace::Properties(properties) => {
853 assert_eq!(properties.get_string("missingValue"), None);
854 }
855 _ => panic!("Expected Properties namespace"),
856 },
857 Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
858 }
859 }
860
861 #[cfg(not(target_arch = "wasm32"))]
862 #[tokio::test]
863 async fn test_string_value() {
864 setup();
865 let namespace::Namespace::Properties(properties) =
866 CLIENT_NO_SECRET.namespace("application").await.unwrap()
867 else {
868 panic!("Expected Properties namespace");
869 };
870
871 assert_eq!(
872 properties.get_property::<String>("stringValue"),
873 Some("string value".to_string())
874 );
875 }
876
877 #[cfg(target_arch = "wasm32")]
878 #[wasm_bindgen_test::wasm_bindgen_test]
879 #[allow(dead_code)]
880 async fn test_string_value_wasm() {
881 setup();
882 let client = create_client_no_secret();
883 let namespace = client.namespace("application").await;
884 match namespace {
885 Ok(namespace) => match namespace {
886 namespace::Namespace::Properties(properties) => {
887 assert_eq!(
888 properties.get_string("stringValue"),
889 Some("string value".to_string())
890 );
891 }
892 _ => panic!("Expected Properties namespace"),
893 },
894 Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
895 }
896 }
897
898 #[cfg(not(target_arch = "wasm32"))]
899 #[tokio::test]
900 async fn test_string_value_with_secret() {
901 setup();
902 let namespace::Namespace::Properties(properties) =
903 CLIENT_WITH_SECRET.namespace("application").await.unwrap()
904 else {
905 panic!("Expected Properties namespace");
906 };
907 assert_eq!(
908 properties.get_property::<String>("stringValue"),
909 Some("string value".to_string())
910 );
911 }
912
913 #[cfg(target_arch = "wasm32")]
914 #[wasm_bindgen_test::wasm_bindgen_test]
915 #[allow(dead_code)]
916 async fn test_string_value_with_secret_wasm() {
917 setup();
918 let client = create_client_with_secret();
919 let namespace = client.namespace("application").await;
920 match namespace {
921 Ok(namespace) => match namespace {
922 namespace::Namespace::Properties(properties) => {
923 assert_eq!(
924 properties.get_string("stringValue"),
925 Some("string value".to_string())
926 );
927 }
928 _ => panic!("Expected Properties namespace"),
929 },
930 Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
931 }
932 }
933
934 #[cfg(not(target_arch = "wasm32"))]
935 #[tokio::test]
936 async fn test_int_value() {
937 setup();
938 let namespace::Namespace::Properties(properties) =
939 CLIENT_NO_SECRET.namespace("application").await.unwrap()
940 else {
941 panic!("Expected Properties namespace");
942 };
943 assert_eq!(properties.get_property::<i32>("intValue"), Some(42));
944 }
945
946 #[cfg(target_arch = "wasm32")]
947 #[wasm_bindgen_test::wasm_bindgen_test]
948 #[allow(dead_code)]
949 async fn test_int_value_wasm() {
950 setup();
951 let client = create_client_no_secret();
952 let namespace = client.namespace("application").await;
953 match namespace {
954 Ok(namespace) => match namespace {
955 namespace::Namespace::Properties(properties) => {
956 assert_eq!(properties.get_int("intValue"), Some(42));
957 }
958 _ => panic!("Expected Properties namespace"),
959 },
960 Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
961 }
962 }
963
964 #[cfg(not(target_arch = "wasm32"))]
965 #[tokio::test]
966 async fn test_int_value_with_secret() {
967 setup();
968 let namespace::Namespace::Properties(properties) =
969 CLIENT_WITH_SECRET.namespace("application").await.unwrap()
970 else {
971 panic!("Expected Properties namespace");
972 };
973 assert_eq!(properties.get_property::<i32>("intValue"), Some(42));
974 }
975
976 #[cfg(target_arch = "wasm32")]
977 #[wasm_bindgen_test::wasm_bindgen_test]
978 #[allow(dead_code)]
979 async fn test_int_value_with_secret_wasm() {
980 setup();
981 let client = create_client_with_secret();
982 let namespace = client.namespace("application").await;
983 match namespace {
984 Ok(namespace) => match namespace {
985 namespace::Namespace::Properties(properties) => {
986 assert_eq!(properties.get_int("intValue"), Some(42));
987 }
988 _ => panic!("Expected Properties namespace"),
989 },
990 Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
991 }
992 }
993
994 #[cfg(not(target_arch = "wasm32"))]
995 #[tokio::test]
996 async fn test_float_value() {
997 setup();
998 let namespace::Namespace::Properties(properties) =
999 CLIENT_NO_SECRET.namespace("application").await.unwrap()
1000 else {
1001 panic!("Expected Properties namespace");
1002 };
1003 assert_eq!(properties.get_property::<f64>("floatValue"), Some(4.20));
1004 }
1005
1006 #[cfg(target_arch = "wasm32")]
1007 #[wasm_bindgen_test::wasm_bindgen_test]
1008 #[allow(dead_code)]
1009 async fn test_float_value_wasm() {
1010 setup();
1011 let client = create_client_no_secret();
1012 let namespace = client.namespace("application").await;
1013 match namespace {
1014 Ok(namespace) => match namespace {
1015 namespace::Namespace::Properties(properties) => {
1016 assert_eq!(properties.get_float("floatValue"), Some(4.20));
1017 }
1018 _ => panic!("Expected Properties namespace"),
1019 },
1020 Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
1021 }
1022 }
1023
1024 #[cfg(not(target_arch = "wasm32"))]
1025 #[tokio::test]
1026 async fn test_float_value_with_secret() {
1027 setup();
1028 let namespace::Namespace::Properties(properties) =
1029 CLIENT_WITH_SECRET.namespace("application").await.unwrap()
1030 else {
1031 panic!("Expected Properties namespace");
1032 };
1033 assert_eq!(properties.get_property::<f64>("floatValue"), Some(4.20));
1034 }
1035
1036 #[cfg(target_arch = "wasm32")]
1037 #[wasm_bindgen_test::wasm_bindgen_test]
1038 #[allow(dead_code)]
1039 async fn test_float_value_with_secret_wasm() {
1040 setup();
1041 let client = create_client_with_secret();
1042 let namespace = client.namespace("application").await;
1043 match namespace {
1044 Ok(namespace) => match namespace {
1045 namespace::Namespace::Properties(properties) => {
1046 assert_eq!(properties.get_float("floatValue"), Some(4.20));
1047 }
1048 _ => panic!("Expected Properties namespace"),
1049 },
1050 Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
1051 }
1052 }
1053
1054 #[cfg(not(target_arch = "wasm32"))]
1055 #[tokio::test]
1056 async fn test_bool_value() {
1057 setup();
1058 let namespace::Namespace::Properties(properties) =
1059 CLIENT_NO_SECRET.namespace("application").await.unwrap()
1060 else {
1061 panic!("Expected Properties namespace");
1062 };
1063 assert_eq!(properties.get_property::<bool>("boolValue"), Some(false));
1064 }
1065
1066 #[cfg(target_arch = "wasm32")]
1067 #[wasm_bindgen_test::wasm_bindgen_test]
1068 #[allow(dead_code)]
1069 async fn test_bool_value_wasm() {
1070 setup();
1071 let client = create_client_no_secret();
1072 let namespace = client.namespace("application").await;
1073 match namespace {
1074 Ok(namespace) => match namespace {
1075 namespace::Namespace::Properties(properties) => {
1076 assert_eq!(properties.get_bool("boolValue"), Some(false));
1077 }
1078 _ => panic!("Expected Properties namespace"),
1079 },
1080 Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
1081 }
1082 }
1083
1084 #[cfg(not(target_arch = "wasm32"))]
1085 #[tokio::test]
1086 async fn test_bool_value_with_secret() {
1087 setup();
1088 let namespace::Namespace::Properties(properties) =
1089 CLIENT_WITH_SECRET.namespace("application").await.unwrap()
1090 else {
1091 panic!("Expected Properties namespace");
1092 };
1093 assert_eq!(properties.get_property::<bool>("boolValue"), Some(false));
1094 }
1095
1096 #[cfg(target_arch = "wasm32")]
1097 #[wasm_bindgen_test::wasm_bindgen_test]
1098 #[allow(dead_code)]
1099 async fn test_bool_value_with_secret_wasm() {
1100 setup();
1101 let client = create_client_with_secret();
1102 let namespace = client.namespace("application").await;
1103 match namespace {
1104 Ok(namespace) => match namespace {
1105 namespace::Namespace::Properties(properties) => {
1106 assert_eq!(properties.get_bool("boolValue"), Some(false));
1107 }
1108 _ => panic!("Expected Properties namespace"),
1109 },
1110 Err(e) => panic!("Expected Properties namespace, got error: {e:?}"),
1111 }
1112 }
1113
1114 #[cfg(not(target_arch = "wasm32"))]
1115 #[tokio::test]
1116 async fn test_bool_value_with_grayscale_ip() {
1117 setup();
1118 let namespace::Namespace::Properties(properties) = CLIENT_WITH_GRAYSCALE_IP
1119 .namespace("application")
1120 .await
1121 .unwrap()
1122 else {
1123 panic!("Expected Properties namespace");
1124 };
1125 assert_eq!(
1126 properties.get_property::<bool>("grayScaleValue"),
1127 Some(true)
1128 );
1129 let namespace::Namespace::Properties(properties) =
1130 CLIENT_NO_SECRET.namespace("application").await.unwrap()
1131 else {
1132 panic!("Expected Properties namespace");
1133 };
1134 assert_eq!(
1135 properties.get_property::<bool>("grayScaleValue"),
1136 Some(false)
1137 );
1138 }
1139
1140 #[cfg(target_arch = "wasm32")]
1141 #[wasm_bindgen_test::wasm_bindgen_test]
1142 #[allow(dead_code)]
1143 async fn test_bool_value_with_grayscale_ip_wasm() {
1144 setup();
1145 let client1 = create_client_with_grayscale_ip();
1146 let namespace = client1.namespace("application").await;
1147 match namespace {
1148 Ok(namespace) => match namespace {
1149 namespace::Namespace::Properties(properties) => {
1150 assert_eq!(properties.get_bool("grayScaleValue"), Some(true));
1151 }
1152 _ => panic!("Expected Properties namespace"),
1153 },
1154 Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
1155 }
1156
1157 let client2 = create_client_no_secret();
1158 let namespace = client2.namespace("application").await;
1159 match namespace {
1160 Ok(namespace) => match namespace {
1161 namespace::Namespace::Properties(properties) => {
1162 assert_eq!(properties.get_bool("grayScaleValue"), Some(false));
1163 }
1164 _ => panic!("Expected Properties namespace"),
1165 },
1166 Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
1167 }
1168 }
1169
1170 #[cfg(not(target_arch = "wasm32"))]
1171 #[tokio::test]
1172 async fn test_bool_value_with_grayscale_label() {
1173 setup();
1174 let namespace::Namespace::Properties(properties) = CLIENT_WITH_GRAYSCALE_LABEL
1175 .namespace("application")
1176 .await
1177 .unwrap()
1178 else {
1179 panic!("Expected Properties namespace");
1180 };
1181 assert_eq!(
1182 properties.get_property::<bool>("grayScaleValue"),
1183 Some(true)
1184 );
1185 let namespace::Namespace::Properties(properties) =
1186 CLIENT_NO_SECRET.namespace("application").await.unwrap()
1187 else {
1188 panic!("Expected Properties namespace");
1189 };
1190 assert_eq!(
1191 properties.get_property::<bool>("grayScaleValue"),
1192 Some(false)
1193 );
1194 }
1195
1196 #[cfg(target_arch = "wasm32")]
1197 #[wasm_bindgen_test::wasm_bindgen_test]
1198 #[allow(dead_code)]
1199 async fn test_bool_value_with_grayscale_label_wasm() {
1200 setup();
1201 let client1 = create_client_with_grayscale_label();
1202 let namespace = client1.namespace("application").await;
1203 match namespace {
1204 Ok(namespace) => match namespace {
1205 namespace::Namespace::Properties(properties) => {
1206 assert_eq!(properties.get_bool("grayScaleValue"), Some(true));
1207 }
1208 _ => panic!("Expected Properties namespace"),
1209 },
1210 Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
1211 }
1212
1213 let client2 = create_client_no_secret();
1214 let namespace = client2.namespace("application").await;
1215 match namespace {
1216 Ok(namespace) => match namespace {
1217 namespace::Namespace::Properties(properties) => {
1218 assert_eq!(properties.get_bool("grayScaleValue"), Some(false));
1219 }
1220 _ => panic!("Expected Properties namespace"),
1221 },
1222 Err(e) => panic!("Expected Properties namespace, got error: {:?}", e),
1223 }
1224 }
1225
1226 #[cfg(target_arch = "wasm32")]
1227 fn create_client_no_secret() -> Client {
1228 let config = ClientConfig {
1229 app_id: String::from("101010101"),
1230 cluster: String::from("default"),
1231 config_server: test_server_url(),
1232 label: None,
1233 secret: None,
1234 cache_dir: None,
1235 ip: None,
1236 allow_insecure_https: None,
1237 };
1238 Client::new(config)
1239 }
1240
1241 #[cfg(target_arch = "wasm32")]
1242 fn create_client_with_secret() -> Client {
1243 let config = ClientConfig {
1244 app_id: String::from("101010102"),
1245 cluster: String::from("default"),
1246 config_server: test_server_url(),
1247 label: None,
1248 secret: Some(String::from("53bf47631db540ac9700f0020d2192c8")),
1249 cache_dir: None,
1250 ip: None,
1251 allow_insecure_https: None,
1252 };
1253 Client::new(config)
1254 }
1255
1256 #[cfg(target_arch = "wasm32")]
1257 fn create_client_with_grayscale_ip() -> Client {
1258 let config = ClientConfig {
1259 app_id: String::from("101010101"),
1260 cluster: String::from("default"),
1261 config_server: test_server_url(),
1262 label: None,
1263 secret: None,
1264 cache_dir: None,
1265 ip: Some(String::from("1.2.3.4")),
1266 allow_insecure_https: None,
1267 };
1268 Client::new(config)
1269 }
1270
1271 #[cfg(target_arch = "wasm32")]
1272 fn create_client_with_grayscale_label() -> Client {
1273 let config = ClientConfig {
1274 app_id: String::from("101010101"),
1275 cluster: String::from("default"),
1276 config_server: test_server_url(),
1277 label: Some(String::from("GrayScale")),
1278 secret: None,
1279 cache_dir: None,
1280 ip: None,
1281 allow_insecure_https: None,
1282 };
1283 Client::new(config)
1284 }
1285
1286 #[cfg(not(target_arch = "wasm32"))]
1287 #[tokio::test] async fn test_add_listener_and_notify_on_refresh() {
1289 setup();
1290
1291 let listener_called_flag = Arc::new(Mutex::new(false));
1293 let received_config_data = Arc::new(Mutex::new(None::<Namespace>));
1294
1295 let temp_dir = TempDir::new("apollo_listener_test");
1296
1297 let config = ClientConfig {
1300 config_server: test_server_url(), app_id: "101010101".to_string(), cluster: "default".to_string(),
1303 cache_dir: Some(temp_dir.path().to_str().unwrap().to_string()), secret: None,
1305 label: None,
1306 ip: None,
1307 allow_insecure_https: None,
1308 #[cfg(not(target_arch = "wasm32"))]
1309 cache_ttl: None,
1310 #[cfg(not(target_arch = "wasm32"))]
1311 refresh_interval: None,
1312 #[cfg(not(target_arch = "wasm32"))]
1313 http_client: None,
1314 };
1315
1316 let client = Client::new(config);
1317
1318 let flag_clone = listener_called_flag.clone();
1319 let data_clone = received_config_data.clone();
1320
1321 let listener: EventListener = Arc::new(move |result| {
1322 let mut called_guard = flag_clone.lock().unwrap();
1323 *called_guard = true;
1324 if let Ok(config_value) = result {
1325 match config_value {
1326 Namespace::Properties(_) => {
1327 let mut data_guard = data_clone.lock().unwrap();
1328 *data_guard = Some(config_value.clone());
1329 }
1330 _ => {
1331 panic!("Expected Properties namespace, got {config_value:?}");
1332 }
1333 }
1334 }
1335 });
1338
1339 client.add_listener("application", listener).await;
1340
1341 let cache = client.cache("application").await;
1342
1343 match cache.refresh().await {
1346 Ok(()) => log::debug!("Refresh successful for test_add_listener_and_notify_on_refresh"),
1347 Err(e) => panic!("Cache refresh failed during test: {e:?}"),
1348 }
1349
1350 cfg_if::cfg_if! {
1352 if #[cfg(target_arch = "wasm32")] {
1353 } else {
1355 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1357 }
1358 }
1359
1360 let called = *listener_called_flag.lock().unwrap();
1362 assert!(called, "Listener was not called.");
1363
1364 let config_data_guard = received_config_data.lock().unwrap();
1366 assert!(
1367 config_data_guard.is_some(),
1368 "Listener did not receive config data."
1369 );
1370
1371 if let Some(value) = config_data_guard.as_ref() {
1375 match value {
1376 Namespace::Properties(properties) => {
1377 assert_eq!(
1378 properties.get_string("stringValue"),
1379 Some(String::from("string value")),
1380 "Received config data does not match expected content for stringValue."
1381 );
1382 }
1383 _ => {
1384 panic!("Expected Properties namespace, got {value:?}");
1385 }
1386 }
1387 }
1388 }
1389
1390 #[cfg(target_arch = "wasm32")]
1391 #[wasm_bindgen_test::wasm_bindgen_test]
1392 async fn test_add_listener_wasm_and_notify() {
1393 setup(); let listener_called_flag = Arc::new(Mutex::new(false));
1397 let received_config_data = Arc::new(Mutex::new(None::<Namespace>));
1398
1399 let flag_clone = listener_called_flag.clone();
1400 let data_clone = received_config_data.clone();
1401
1402 let js_listener_func_body = format!(
1404 r#"
1405 (data, error) => {{
1406 // We can't use window in Node.js, so we'll use a different approach
1407 // The Rust closure will handle the verification
1408 console.log('JS Listener called with error:', error);
1409 console.log('JS Listener called with data:', data);
1410 }}
1411 "#
1412 );
1413
1414 let js_listener = js_sys::Function::new_with_args("data, error", &js_listener_func_body);
1415
1416 let client = create_client_no_secret();
1417
1418 let rust_listener: EventListener = Arc::new(move |result| {
1420 let mut called_guard = flag_clone.lock().unwrap();
1421 *called_guard = true;
1422 if let Ok(config_value) = result {
1423 let mut data_guard = data_clone.lock().unwrap();
1424 *data_guard = Some(config_value);
1425 }
1426 });
1427
1428 client.add_listener("application", rust_listener).await;
1429
1430 client.add_listener_wasm("application", js_listener).await;
1432
1433 let cache = client.cache("application").await;
1434
1435 match cache.refresh().await {
1437 Ok(_) => web_sys::console::log_1(&"WASM Test: Refresh successful".into()), Err(e) => panic!("WASM Test: Cache refresh failed: {:?}", e),
1439 }
1440
1441 cfg_if::cfg_if! {
1443 if #[cfg(target_arch = "wasm32")] {
1444 } else {
1446 tokio::time::sleep(std::time::Duration::from_millis(100)).await;
1448 }
1449 }
1450
1451 let called = *listener_called_flag.lock().unwrap();
1453 assert!(called, "Listener was not called.");
1454
1455 let config_data_guard = received_config_data.lock().unwrap();
1457 assert!(
1458 config_data_guard.is_some(),
1459 "Listener did not receive config data."
1460 );
1461
1462 if let Some(value) = config_data_guard.as_ref() {
1464 match value {
1465 namespace::Namespace::Properties(properties) => {
1466 assert_eq!(
1467 properties.get_string("stringValue"),
1468 Some("string value".to_string())
1469 );
1470 }
1471 _ => panic!("Expected Properties namespace"),
1472 }
1473 }
1474 }
1475
1476 #[cfg(not(target_arch = "wasm32"))]
1477 #[tokio::test]
1478 async fn test_concurrent_namespace_hang_repro() {
1479 setup();
1480
1481 let temp_dir = TempDir::new("apollo_hang_test");
1482
1483 let config = ClientConfig {
1484 app_id: String::from("101010101"),
1485 cluster: String::from("default"),
1486 config_server: test_server_url(),
1487 secret: None,
1488 cache_dir: Some(temp_dir.path().to_str().unwrap().to_string()),
1489 label: None,
1490 ip: None,
1491 allow_insecure_https: None,
1492 cache_ttl: None,
1493 refresh_interval: None,
1494 http_client: None,
1495 };
1496
1497 let client = Arc::new(Client::new(config));
1498 let client_in_listener = client.clone();
1499
1500 let listener_triggered = Arc::new(Mutex::new(false));
1501 let listener_triggered_in_listener = listener_triggered.clone();
1502
1503 let listener: EventListener = Arc::new(move |_| {
1504 let client_in_listener = client_in_listener.clone();
1505 let listener_triggered_in_listener = listener_triggered_in_listener.clone();
1506 tokio::spawn(async move {
1507 {
1508 let mut triggered = listener_triggered_in_listener.lock().unwrap();
1509 if *triggered {
1510 return;
1512 }
1513 *triggered = true;
1514 }
1515
1516 let _ = client_in_listener.namespace("application").await;
1518 });
1519 });
1520
1521 client.add_listener("application", listener).await;
1522
1523 let test_body = async {
1524 let _ = client.namespace("application").await;
1525 };
1526
1527 let res = tokio::time::timeout(std::time::Duration::from_secs(10), test_body).await;
1529 assert!(res.is_ok(), "Test timed out, which indicates a deadlock.");
1530 }
1531
1532 #[cfg(not(target_arch = "wasm32"))]
1533 #[tokio::test]
1534 async fn test_custom_refresh_interval() {
1535 setup();
1536
1537 let temp_dir = TempDir::new("apollo_custom_refresh_interval");
1538
1539 let config = ClientConfig {
1540 app_id: String::from("101010101"),
1541 cluster: String::from("default"),
1542 config_server: test_server_url(),
1543 secret: None,
1544 cache_dir: Some(temp_dir.path().to_str().unwrap().to_string()),
1545 label: None,
1546 ip: None,
1547 allow_insecure_https: None,
1548 cache_ttl: None,
1549 refresh_interval: Some(1), http_client: None,
1551 };
1552
1553 let mut client = Client::new(config);
1554 let _ = client.namespace("application").await;
1556
1557 let res = client.start().await;
1558 assert!(res.is_ok(), "Failed to start client background task");
1559
1560 tokio::time::sleep(std::time::Duration::from_millis(2500)).await;
1562
1563 client.stop().await;
1564 }
1565
1566 #[cfg(not(target_arch = "wasm32"))]
1567 #[test]
1568 fn test_refresh_interval_clamping() {
1569 unsafe {
1571 std::env::set_var("APP_ID", "101010101");
1572 std::env::set_var("APOLLO_CONFIG_SERVICE", "http://localhost:8080");
1573
1574 std::env::set_var("APOLLO_REFRESH_INTERVAL", "0");
1576 }
1577 let config = ClientConfig::from_env().unwrap();
1578 assert_eq!(config.refresh_interval, Some(1));
1579
1580 unsafe {
1582 std::env::set_var("APOLLO_REFRESH_INTERVAL", "15");
1583 }
1584 let config2 = ClientConfig::from_env().unwrap();
1585 assert_eq!(config2.refresh_interval, Some(15));
1586
1587 unsafe {
1589 std::env::remove_var("APP_ID");
1590 std::env::remove_var("APOLLO_CONFIG_SERVICE");
1591 std::env::remove_var("APOLLO_REFRESH_INTERVAL");
1592 }
1593 }
1594
1595 #[cfg(target_arch = "wasm32")]
1596 #[wasm_bindgen_test::wasm_bindgen_test]
1597 async fn test_wasm_local_storage_caching() {
1598 use wasm_bindgen::prelude::Closure;
1599 setup();
1600
1601 let store = Arc::new(Mutex::new(HashMap::<String, String>::new()));
1603
1604 let store_clone1 = store.clone();
1605 let get_item = Closure::wrap(Box::new(move |key: String| -> wasm_bindgen::JsValue {
1606 let map = store_clone1.lock().unwrap();
1607 if let Some(val) = map.get(&key) {
1608 wasm_bindgen::JsValue::from_str(val)
1609 } else {
1610 wasm_bindgen::JsValue::NULL
1611 }
1612 }) as Box<dyn Fn(String) -> wasm_bindgen::JsValue>);
1613
1614 let store_clone2 = store.clone();
1615 let set_item = Closure::wrap(Box::new(move |key: String, value: String| {
1616 let mut map = store_clone2.lock().unwrap();
1617 map.insert(key, value);
1618 }) as Box<dyn Fn(String, String)>);
1619
1620 let mock_storage = js_sys::Object::new();
1621 js_sys::Reflect::set(&mock_storage, &wasm_bindgen::JsValue::from_str("getItem"), get_item.as_ref()).unwrap();
1622 js_sys::Reflect::set(&mock_storage, &wasm_bindgen::JsValue::from_str("setItem"), set_item.as_ref()).unwrap();
1623
1624 let global = js_sys::global();
1626 js_sys::Reflect::set(&global, &wasm_bindgen::JsValue::from_str("localStorage"), &mock_storage).unwrap();
1627
1628 let config = ClientConfig {
1630 app_id: "101010101".to_string(),
1631 cluster: "default".to_string(),
1632 config_server: "http://localhost:8080".to_string(),
1633 secret: None,
1634 cache_dir: None,
1635 label: None,
1636 ip: None,
1637 allow_insecure_https: None,
1638 };
1639
1640 let cache_item = serde_json::json!({
1642 "timestamp": chrono::Utc::now().timestamp(),
1643 "config": {
1644 "stringValue": "localstorage value"
1645 }
1646 });
1647 let cache_content = serde_json::to_string(&cache_item).unwrap();
1648
1649 let cache_key = "apollo_cache_101010101_default_application";
1651 {
1652 let mut map = store.lock().unwrap();
1653 map.insert(cache_key.to_string(), cache_content);
1654 }
1655
1656 let cache = cache::Cache::new(
1658 config,
1659 "application",
1660 reqwest::Client::new(),
1661 );
1662
1663 let value = cache.get_value().await.unwrap();
1665 assert_eq!(
1666 value.get("stringValue").and_then(|v| v.as_str()),
1667 Some("localstorage value"),
1668 "Cache failed to load configuration from mocked local storage"
1669 );
1670
1671 get_item.into_js_value();
1673 set_item.into_js_value();
1674
1675 let _ = js_sys::Reflect::delete_property(&global, &wasm_bindgen::JsValue::from_str("localStorage"));
1677 }
1678
1679 #[cfg(target_arch = "wasm32")]
1680 #[wasm_bindgen_test::wasm_bindgen_test]
1681 fn test_wasm_cache_key_isolation() {
1682 setup();
1683
1684 let config1 = ClientConfig {
1686 app_id: "app1".to_string(),
1687 cluster: "default".to_string(),
1688 config_server: "http://localhost:8080".to_string(),
1689 secret: None,
1690 cache_dir: None,
1691 label: None,
1692 ip: None,
1693 allow_insecure_https: None,
1694 };
1695 let cache1 = cache::Cache::new(config1, "application", reqwest::Client::new());
1696 assert_eq!(cache1.wasm_cache_key(), "apollo_cache_app1_default_application");
1697
1698 let config2 = ClientConfig {
1700 app_id: "app1".to_string(),
1701 cluster: "prod".to_string(),
1702 config_server: "http://localhost:8080".to_string(),
1703 secret: None,
1704 cache_dir: None,
1705 label: None,
1706 ip: None,
1707 allow_insecure_https: None,
1708 };
1709 let cache2 = cache::Cache::new(config2, "application", reqwest::Client::new());
1710 assert_eq!(cache2.wasm_cache_key(), "apollo_cache_app1_prod_application");
1711
1712 let config3 = ClientConfig {
1714 app_id: "app1".to_string(),
1715 cluster: "default".to_string(),
1716 config_server: "http://localhost:8080".to_string(),
1717 secret: None,
1718 cache_dir: None,
1719 label: None,
1720 ip: None,
1721 allow_insecure_https: None,
1722 };
1723 let cache3 = cache::Cache::new(config3, "other_namespace", reqwest::Client::new());
1724 assert_eq!(cache3.wasm_cache_key(), "apollo_cache_app1_default_other_namespace");
1725
1726 let config4 = ClientConfig {
1728 app_id: "app1".to_string(),
1729 cluster: "default".to_string(),
1730 config_server: "http://localhost:8080".to_string(),
1731 secret: None,
1732 cache_dir: None,
1733 label: Some("gray".to_string()),
1734 ip: Some("192.168.1.1".to_string()),
1735 allow_insecure_https: None,
1736 };
1737 let cache4 = cache::Cache::new(config4, "application", reqwest::Client::new());
1738 assert_eq!(cache4.wasm_cache_key(), "apollo_cache_app1_default_application_192.168.1.1_gray");
1739 }
1740
1741 #[cfg(target_arch = "wasm32")]
1742 #[wasm_bindgen_test::wasm_bindgen_test]
1743 fn test_wasm_allow_insecure_https_warning() {
1744 setup();
1745
1746 let config = ClientConfig {
1747 app_id: "101010101".to_string(),
1748 cluster: "default".to_string(),
1749 config_server: "http://localhost:8080".to_string(),
1750 secret: None,
1751 cache_dir: None,
1752 label: None,
1753 ip: None,
1754 allow_insecure_https: Some(true),
1755 };
1756
1757 let _client = Client::new(config);
1759 }
1760
1761 #[cfg(not(target_arch = "wasm32"))]
1762 #[tokio::test]
1763 async fn test_custom_http_client_injection() {
1764 setup();
1765 let custom_client = reqwest::Client::builder()
1767 .timeout(std::time::Duration::from_millis(1))
1768 .build()
1769 .unwrap();
1770
1771 let temp_dir = TempDir::new("apollo_custom_http_test");
1772
1773 let config = ClientConfig {
1774 config_server: test_server_url(),
1775 app_id: "101010101".to_string(),
1776 cluster: "default".to_string(),
1777 cache_dir: Some(temp_dir.path().to_str().unwrap().to_string()),
1778 secret: None,
1779 label: None,
1780 ip: None,
1781 allow_insecure_https: None,
1782 cache_ttl: None,
1783 refresh_interval: None,
1784 http_client: Some(custom_client),
1785 };
1786
1787 let client = Client::new(config);
1788
1789 let result = client.namespace("application").await;
1791 assert!(result.is_err(), "Expected request to fail due to custom injected HTTP client timeout");
1792
1793 let err_str = result.err().unwrap().to_string();
1795 assert!(err_str.contains("timeout") || err_str.contains("error") || err_str.contains("reqwest"), "Expected error to mention timeout or request failure, got: {err_str}");
1796 }
1797}
1798